134
Часть I. Язык C#
Методы
Как упоминалось выше, переменные экземпляров и методы — две основные составляющие классов. Пока наш класс
Building содержит только данные. Хотя такие классы (без методов) вполне допустимы, большинство классов имеют методы. Методы — это процедуры (подпрограммы), которые манипулируют данными, определенными в классе, и во многих случаях обеспечивают доступ к этим данным. Обычно различные части программы взаимодействуют с классом посредством его методов.
Любой метод содержит одну или несколько инструкций. В хорошей C#-программе один метод выполняет только одну задачу. Каждый метод имеет имя, и именно это имя используется для его вызова. В общем случае методу можно присвоить любое имя. Но помните, что имя
Main()
зарезервировано для метода, с которого начинается выполнение программы. Кроме того, в качестве имен методов нельзя использовать ключевые слова C#.
Имена методов в тексте этой книги сопровождаются парой круглых скобок.
Например, если метод имеет имя getval
, то в тексте будет написано getval().
Это помогает отличать имена переменных от имен методов.
Формат записи метода такой:
доступ
тип_возврата
имя
(
список_параметров
) {
// тело метода
}
Здесь элемент
доступ
означает модификатор доступа, который определяет, какие части программы могут получить доступ к методу. Как упоминалось выше, модификатор доступа необязателен, и, если он не указан, подразумевается, что метод закрыт (
private
) в рамках класса, где он определен. Пока мы будем объявлять все методы как public
- члены, чтобы их могли вызывать все остальные составные части программного кода, даже те, которые определены вне класса.
С помощью элемента
тип_возврата
указывается тип значения, возвращаемого методом. Это может быть любой допустимый тип, включая типы классов, создаваемые программистом. Если метод не возвращает никакого значения, необходимо указать тип void
. Имя метода, как нетрудно догадаться, задается элементом
имя
. В качестве имени метода можно использовать любой допустимый идентификатор, отличный от тех, которые уже использованы для других элементов программы в пределах текущей области видимости. Элемент
список_параметров
представляет собой последовательность пар
(состоящих из типа данных и идентификатора), разделенных запятыми. Параметры — это переменные, которые получают значения аргументов, передаваемых методу при вызове.
Если метод не имеет параметров,
список_параметров
остается пустым.
Добавление методов в класс Building
Как вам уже известно, методы класса, как правило, манипулируют данными, определенными в классе, и обеспечивают доступ к этим данным. Зная это, вспомним, что метод
Main()
в предыдущей программе вычислял площадь, приходящуюся на одного человека, путем деления общей площади здания на количество жильцов.
Несмотря на формальную корректность, эти вычисления выполнены не самым удачным образом. Ведь с вычислением площади, приходящейся на одного человека, вполне может справиться сам класс
Building
, поскольку эта величина зависит только от значений переменных area и occupants, которые инкапсулированы в классе
Building
. Как говорится, сам Бог велел классу
Building выполнить это арифметическое
Глава 6. Введение в классы, объекты и методы
135 действие. Более того, если оно таки будет “закреплено” за этим классом, то другой программе, которая его использует, не придется делать это действие “вручную”. Здесь налицо не просто удобство для “других” программ, а предотвращение неоправданного дублирования кода. Наконец, внося в класс
Building метод, который вычисляет площадь, приходящуюся на одного человека, вы
улучшаете его объектно-ориентированную структуру, инкапсулируя внутри рассматриваемого класса величины, связанные непосредственно со зданием.
Чтобы добавить в класс
Building метод, необходимо определить его внутри объявления класса. Например, следующая версия класса
Building содержит метод с именем areaPerPerson()
, который отображает значение площади конкретного здания, приходящейся на одного человека.
// Добавление метода в класс Building. using System; class Building { public int floors; // количество этажей public int area; // общая площадь здания public int occupants; // количество жильцов
//
Отображаем значение площади, приходящейся
//на одного человека. public void areaPerPerson() {
Console.WriteLine(" " + area / occupants +
" приходится на одного человека");
}
}
// Используем метод areaPerPerson(). class BuildingDemo { public static void Main() {
Building house = new Building();
Building office = new Building();
//
Присваиваем значения полям в объекте house. house.occupants = 4; house.area = 2500; house.floors = 2;
//
Присваиваем значения полям в объекте office. office.occupants = 25; office.area = 4200; office.floors = 3;
Console.WriteLine("Дом имеет:\n " + house.floors
+
" этажа\n " + house.occupants
+
" жильца\n " + house.area
+
" квадратных футов общей площади, из них"); house.areaPerPerson();
Console.WriteLine();
Console.WriteLine("Офис имеет:\n " +
136
Часть I. Язык C# office.floors
+
" этажа\n " + office.occupants
+
" работников\n " + office.area
+
" квадратных футов общей площади, из них"); office.areaPerPerson();
}
}
Эта программа генерирует результаты, которые совпадают с предыдущими:
Дом имеет:
2 этажа
4 жильца
2500 квадратных футов общей площади, из них
625 приходится на одного человека
Офис имеет:
3 этажа
25 работников
4200 квадратных футов общей площади, из них
168 приходится на одного человека
Теперь рассмотрим ключевые элементы этой программы, начиная с самого метода areaPerPerson()
. Первая строка этого метода выглядит так: public void areaPerPerson() {
В этой строке объявляется метод с именем areaPerPerson()
, который не имеет параметров. Этот метод определен с использованием спецификатора доступа public
, поэтому его могут использовать все остальные части программы. Метод areaPerPerson()
возвращает значение типа void
, т.е. не возвращает никакого значения. Эта строка завершается открывающей фигурной скобкой, за которой должно находиться тело метода.
Так и есть. Тело метода areaPerPerson()
состоит из единственной инструкции:
Console.WriteLine(" " + area / occupants +
" приходится на одного человека");
Эта инструкция отображает площадь здания, которая приходится на одного человека, путем деления значения переменной area на значение переменной occupants
Поскольку каждый объект типа
Building имеет собственную копию значений area и occupants
, то при вызове метода areaPerPerson()
в вычислении площади здания, которая приходится на одного человека, будут использоваться копии этих переменных, принадлежащие конкретному вызывающему объекту.
Метод areaPerPerson()
завершается закрывающей фигурной скобкой, т.е. при обнаружении закрывающей фигурной скобки управление программой передается вызывающему объекту.
Теперь рассмотрим внимательнее строку кода из метода
Main()
: house.areaPerPerson();
Эта инструкция вызывает метод areaPerPerson()
для объекта house
. Как видите, для этого используется имя объекта, за которым следует оператор “точка”. При вызове метода управление выполнением программы передается телу метода, а после его завершения управление возвращается автору вызова, и выполнение программы возобновляется со строки кода, которая расположена сразу за вызовом метода.
В данном случае в результате вызова house.areaPerPerson()
отображается значение площади, которая приходится на одного человека, для здания, определенного объектом house
. Точно так же в результате вызова office.areaPerPerson()
отображается значение площади, которая приходится на одного человека, для здания,
Глава 6. Введение в классы, объекты и методы
137 определенного объектом office
. Другими словами, каждый раз, когда вызывается метод areaPerPerson()
, отображается значение площади, которая приходится на одного человека, для здания, описываемого заданным объектом.
Обратите внимание вот на что. Переменные экземпляра area и occupants используются внутри метода areaPerPerson()
без
каких бы то ни было атрибутов, т.е. им не предшествует ни имя объекта, ни оператор “точка”. Это очень важный момент: если метод задействует переменную экземпляра, которая определена в его классе, он делает это напрямую, без явной ссылки на объект и без оператора “точка”. И это логично, Ведь метод всегда вызывается для некоторого объекта конкретного класса. И если уж вызов состоялся, объект, стало быть, известен. Таким образом, нет необходимости указывать внутри метода объект во второй раз. Это значит, что значения area и occupants внутри метода areaPerPerson()
неявно указывают на копии этих переменных, принадлежащих объекту, который вызывает метод areaPerPerson()
Возвращение из метода В общем случае существует два варианта условий для возвращения из метода.
Первый связан с обнаружением закрывающей фигурной скобки, обозначающей конец тела метода (как продемонстрировано на примере метода areaPerPerson()
), Второй вариант состоит в выполнении инструкции return
. Возможны две формы использования инструкции return
: одна предназначена для void
-методов (которые не возвращают значений), а другая — для возврата значений. В этом разделе мы рассмотрим первую форму, а в следующем — вторую.
Немедленное завершение void
-метода можно организовать с помощью следующей формы инструкции return
: return;
При выполнении этой инструкции управление программой передается автору вызова метода, а оставшийся код опускается. Рассмотрим, например, следующий метод: public void myMeth() { int i; for(i=0; i<10; i++) { if(i == 5) return; // Прекращение выполнения
// метода при i = 5.
Console.WriteLine();
}
}
Здесь цикл for будет работать при значениях i
в диапазоне только от 0 до 5, поскольку, как только значение i
станет равным 5, будет выполнен возврат из метода myMeth()
Метод может иметь несколько инструкций return
. Например, выход из метода public void myMeth() {
//... if(done) return;
//... if(error) return;
} произойдет либо в случае его корректного завершения, либо при возникновении ошибки. Однако наличие слишком большого количества точек выхода из метода может деструктурировать код. Поэтому, несмотря на допустимость их множественного применения, следует все же использовать эту возможность с большой осторожностью.
138
Часть I. Язык C#
Итак, завяжем “узелок на память”: выход из void
-метода может быть осуществлен двумя способами: по достижении закрывающей фигурной скобки или при выполнении инструкции return.
Возврат значения
Хотя void
-методы — не редкость, большинство методов все же возвращают значение. И в самом деле, способность возвращать значение — одно из самых полезных качеств метода. Мы уже рассматривали пример возврата значения методом в главе 3, когда использовали метод
Math.Sqrt()
для получения квадратного корня.
Значения, возвращаемые методами, используются в программировании по-разному. В одних случаях (как в методе
Math.Sqrt()
) возвращаемое значение является результатом вычислений, в других — оно просто означает, успешно или нет выполнены действия, составляющие метод, а в третьих — оно может представлять собой код состояния. Однако независимо от цели применения, использование значений, возвращаемых методами, является неотъемлемой частью C#-программирования.
Методы возвращают значения вызывающим их процедурам, используя следующую форму инструкции return
: return
значение
;
Здесь элемент
значение
и представляет значение, возвращаемое методом.
Способность методов возвращать значения можно использовать для улучшения реализации метода areaPerPerson()
. Вместо того чтобы отображать значение площади, которая приходится на одного человека, метод areaPerPerson()
будет теперь возвращать это значение, которое можно использовать в других вычислениях. В следующем примере представлен модифицированный вариант метода areaPerPerson()
, который возвращает значение площади, приходящейся на одного человека, а не отображает его (как в предыдущем варианте).
// Демонстрация возврата значения методом areaPerPerson(). using System; class Building { public int floors; // количество этажей public int area; // общая площадь здания public int occupants; // количество жильцов
//
Возврат значения площади, которая
// приходится на одного человека. public int areaPerPerson() { return area / occupants;
}
}
// Использование значения от метода areaPerPerson(). class BuildingDemo { public static void Main() {
Building house = new Building();
Building office = new Building(); int areaPP; // Площадь, которая приходится
// на одного человека.
// присваиваем значения полям в объекте house. house.occupants = 4; house.area = 2500; house.floors = 2;
Глава 6. Введение в классы, объекты и методы
139 //
Присваиваем значения полям в объекте office. office.occupants = 25; office.area = 4200; office.floors = 3;
//
Получаем для объекта house площадь, которая
// приходится на одного человека. areaPP = house.areaPerPerson();
Console.WriteLine("Дом имеет:\n " + house.floors
+
" этажа\n " + house.occupants
+
" жильца\n " + house.area
+
" квадратных футов общей площади, из них\n " + areaPP
+
" приходится на одного человека");
Console.WriteLine();
//
Получаем площадь для объекта office, которая
// приходится на одного человека. areaPP = office.areaPerPerson();
Console.WriteLine("Офис имеет:\n " + office.floors
+
" этажа\n " + office.occupants
+
" работников\n " + office.area
+
" квадратных футов общей площади, из них\n " + areaPP
+
" приходится на одного человека");
}
}
Результаты выполнения этого варианта программы аналогичны предыдущим.
Обратите внимание на вызов метода areaPerPerson()
: его имя находится справа от оператора присваивания. В левой части стоит переменная, которая и получает значение, возвращаемое методом areaPerPerson()
. Таким образом, после выполнения инструкции areaPP = house.areaPerPerson(); значение площади, приходящейся на одного человека для объекта house
, будет сохранено в переменной areaPP
Обратите также внимание на то, что метод areaPerPerson()
имеет в этом примере другой тип возвращаемого значения, а именно тип int
. Это означает, что метод возвращает автору вызова целое число. Тип значения, возвращаемого методом, — очень важная характеристика метода, поскольку тип данных, возвращаемых методом, должен быть
совместимым с типом возвращаемого значения, указанного в заголовке определения метода. Следовательно, если вы хотите, чтобы метод возвращал данные типа double
, при его определении в качестве типа возвращаемого значения следует указать double
Несмотря на корректность предыдущей программы, ее эффективность оставляет желать лучшего. В частности, нет необходимости в использовании переменной areaPP
Вызов метода areaPerPerson()
можно реализовать непосредственно в инструкции вызова метода
WriteLine();
Console.WriteLine("Дом имеет:\n " + house.floors + " этажа\n " +
140
Часть I. Язык C# house.occupants + " жильца\n " + house.area + " общей площади, из них\n " + house.areaPerPerson() +
" приходится на одного человека");
В этом случае при вызове метода
WriteLine()
автоматически вызывается метод house.areaPerPerson()
, и возвращаемое им значение передается методу
WriteLine()
. Более того, теперь обращение к методу house.areaPerPerson() можно использовать везде, где необходимо значение площади для объекта класса
Building
, которая приходится на одного человека. Например, в следующей инструкции сравниваются такие значения площадей двух зданий. if(b1.areaPerPerson() > b2.areaPerPerson())
Console.WriteLine(
"В здании b1 больше места для каждого человека.");
Использование параметров
При вызове методу можно передать одно или несколько значений. Как упоминалось выше, значение, передаваемое методу, называется аргументом. Переменная внутри метода, которая принимает значение аргумента, называется параметром. Параметры объявляются внутри круглых скобок, которые следуют за именем метода. Синтаксис объявления параметров аналогичен синтаксису, применяемому для переменных. Параметр находится в области видимости своего метода, и, помимо специальной задачи получения аргумента, действует подобно любой локальной переменной.
Перед вами простой пример использования метода с параметром. В классе
ChkNum определен метод isPrime
, который возвращает значение true
, если переданное ему значение является простым, и значение false в противном случае. Следовательно, метод isPrime возвращает значение типа bool
// Простой пример использования параметра. using System; class ChkNum {
//
Метод возвращает true, если x - простое число. public bool isPrime(int x) { for(int i=2; i < x/2 + 1; i++) if((x %i) == 0) return false; return true;
}
} class ParmDemo { public static void Main() {
ChkNum ob = new ChkNum(); for(int i=1; i < 10; i++) if(ob.isPrime(i))
Console.WriteLine(i
+
" простое число."); else Console.WriteLine(i + " не простое число.");
}
}
Эта программа генерирует следующие результаты:
Глава 6. Введение в классы, объекты и методы
141
1 простое число.
2 простое число.
3 простое число.
4 не простое число.
5 простое число.
6 не простое число.
7 простое число.
8 не простое число.
9 не простое число.
В этой программе метод isPrime вызывается девять раз, и каждый раз ему передается новое значение. Рассмотрим этот процесс более внимательно. Во-первых, обратите внимание на то, как происходит обращение к методу isPrime
. Передаваемый аргумент указывается между круглыми скобками. При первом вызове метода isPrime ему передается значение 1. Перед началом выполнения этого метода параметр x
получит значение 1. При втором вызове аргумент будет равен числу 2, а значит, и параметр получит значение 2 и т.д. Важно то, что значение, переданное как аргумент при вызове функции isPrime
, представляет собой значение, получаемое параметром x.
Метод может иметь более одного параметра. В этом случае достаточно объявить каждый параметр, отделив его от следующего запятой. Расширим, например, уже знакомый нам по предыдущей программе класс
ChkNum
, добавив в него метод lcd()
, который возвращает наименьший общий знаменатель (least common denominator) для двух передаваемых ему значений.
// Добавляем метод, который принимает два аргумента. using System; class ChkNum {
//
Метод возвращает true, если x - простое число. public bool isPrime(int x) { for(int i=2; i < x/2 + 1; i++) if((x %i) == 0) return false; return true;
}
//
Метод возвращает наименьший общий знаменатель. public int lcd(int a, int b) { int max; if(isPrime(a) | isPrime(b)) return 1; max = a < b ? a : b; for(int i=2; i < max/2 + 1; i++) if(((a%i) == 0) & ((b%i) == 0)) return i; return
1;
}
} class ParmDemo { public static void Main() {
ChkNum ob = new ChkNum(); int a, b; for(int i=1; i < 10; i++)
142
Часть I. Язык C# if(ob.isPrime (i)) Console.WriteLine(i +
" простое число."); else Console.WriteLine(i + " не простое число."); a = 7; b = 8;
Console.WriteLine("Наименьший общий знаменатель для " + a
+
" и " + b + " равен " + ob.lcd(a, b)); a = 100; b = 8;
Console.WriteLine("Наименьший общий знаменатель для " + a
+
" и " + b + " равен " + ob.lcd(a, b)); a = 100; b = 75;
Console.WriteLine("Наименьший общий знаменатель для " + a
+
" и " + b + " равен " + ob.lcd(a, b));
}
}
Обратите внимание на то, что при вызове метода lcd()
аргументы также разделяются запятыми. Вот результаты выполнения этой программы:
1 простое число.
2 простое число.
3 простое число.
4 не простое число.
5 простое число.
6 не простое число.
7 простое число.
8 не простое число.
9 не простое число.
Наименьший общий знаменатель для 7 и 8 равен 1
Наименьший общий знаменатель для 100 и 8 равен 2
Наименьший общий знаменатель для 100 и 75 равен 5
При передаче методу нескольких параметров каждый из них должен сопровождаться указанием собственного типа, причем типы параметров могут быть различными. Например, следующая запись вполне допустима: int myMeth(int a, double b, float с) { // ...
Добавление параметризованного метода в класс Building
Для добавления в класс
Building нового средства (вычисления максимально допустимого количества обитателей здания) можно использовать параметризованный метод. При этом предполагается, что площадь, приходящаяся на каждого человека, не должна быть меньше определенного минимального значения. Назовем этот новый метод maxOccupant()
и приведем его определение.
/* Метод возвращает максимальное количество человек, если на каждого должна приходиться заданная минимальная площадь. */
Глава 6. Введение в классы, объекты и методы
143 public int maxOccupant(int minArea) { return area / minArea;
}
При вызове метода maxOccupant()
параметр minArea получает значение минимальной площади, необходимой для жизнедеятельности каждого человека. Результат, возвращаемый методом maxOccupant()
, получается как частное от деления обшей площади здания на это значение.
Приведем полное определение класса
Building
, включающее метод maxOccupant()
/*
Добавляем параметризованный метод, вычисляющий максимальное количество человек, которые могут занимать это здание в предположении, что на каждого должна приходиться заданная минимальная площадь.
*/ using System; class Building { public int floors; // количество этажей public int area; // общая площадь здания public int occupants; // количество жильцов
//
Метод возвращает площадь, которая приходится
// на одного человека. public int areaPerPerson() { return area / occupants;
}
/*
Метод возвращает максимальное возможное количество человек в здании, если на каждого должна приходиться заданная минимальная площадь. */ public int maxOccupant(int minArea) { return area / minArea;
}
}
// Использование метода maxOccupant(). class BuildingDemo { public static void Main() {
Building house = new Building();
Building office = new Building();
//
Присваиваем значения полям в объекте house. house.occupants = 4; house.area = 2500; house.floors = 2;
//
Присваиваем значения полям в объекте office. office.occupants = 25; office.area = 4200; office.floors = 3;
Console.WriteLine(
"Максимальное
число человек для дома, \n" +
"если на каждого должно приходиться " +
300
+
" квадратных футов: " +
144
Часть I. Язык C# house.maxOccupant(300));
Console.WriteLine(
"Максимальное число человек для офиса, \n" +
"если на каждого должно приходиться " +
300
+
" квадратных футов: " + office.maxOccupant(300));
}
}
Результаты выполнения этой программы выглядят так.
Максимальное число человек для дома, если на каждого должно приходиться 300 квадратных футов: 8
Максимальное число человек для офиса, если на каждого должно приходиться 300 квадратных футов: 14
Как избежать написания недостижимого кода
При создании методов старайтесь не попадать в ситуации, когда часть кода ни при каких обстоятельствах не может быть выполнена. Никогда не выполняемый код называется
недостижимым и считается некорректным в C#. Компилятор при обнаружении такого кода выдаст предупреждающее сообщение. Вот пример: public void m() { char a, b;
// ... if(a==b)
{
Console.WriteLine("равны"); return;
} else
{
Console.WriteLine("не равны"); return;
}
Console.WriteLine("Это недостижимый код.");
}
Здесь последняя инструкция вызова метода
WriteLine()
в методе m()
никогда не будет выполнена, поскольку до нее при любых обстоятельствах будет совершен выход из метода m()
. При попытке скомпилировать этот метод вы получите предупреждение. В общем случае недостижимый код свидетельствует об ошибке с вашей стороны, поэтому имеет смысл серьезно отнестись к предупреждению компилятора.
Конструкторы
В предыдущих примерах переменные каждого
Building
-объекта устанавливались
“вручную” с помощью следующей последовательности инструкций: house.occupants = 4; house.area = 2500; house.floors = 2;
Профессионал никогда бы не использовал подобный подход. И дело не столько в том, что таким образом можно попросту “забыть” об одном или нескольких данных, сколько в том, что существует гораздо более удобный способ это сделать. Этот способ — использование конструктора.
Глава 6. Введение в классы, объекты и методы
145
Конструктор инициализирует объект при его создании. Он имеет такое же имя, что и сам класс, а синтаксически подобен методу. Однако в определении конструкторов не указывается тип возвращаемого значения. Формат записи конструктора такой:
доступ
имя_класса
() {
// тело конструктора
}
Обычно конструктор используется, чтобы придать переменным экземпляра, определенным в классе, начальные значения или выполнить исходные действия, необходимые для создания полностью сформированного объекта. Кроме того, обычно в качестве элемента
доступ
используется модификатор доступа public
, поскольку конструкторы, как правило, вызываются вне их класса,
Все классы имеют конструкторы независимо от того, определите вы их или нет, поскольку C# автоматически предоставляет конструктор по умолчанию, который инициализирует все переменные-члены, имеющие тип значений, нулями, а переменные- члены ссылочного типа — null
-значениями. Но если вы определите собственный конструктор, конструктор по умолчанию больше не используется.
Вот пример использования конструктора:
// Использование простого конструктора. using System; class MyClass { public int x; public MyClass() { x = 10;
}
} class ConsDemo { public static void Main() {
MyClass t1 = new MyClass();
MyClass t2 = new MyClass();
Console.WriteLine(t1.x + " " + t2.x);
}
}
В этом примере программы конструктор класса
MyClass имеет следующий вид: public MyClass() { x = 10;
}
Обратите внимание на public
-определение конструктора, которое позволяет вызывать его из кода, определенного вне класса
MyClass
. Этот конструктор присваивает переменной экземпляра x
значение
10
. Конструктор
MyClass()
вызывается оператором new при создании объекта класса
MyClass
. Например, при выполнении строки
MyClass t1 = new MyClass(); для объекта t1
вызывается конструктор
MyClass()
, который присваивает переменной экземпляра t1.x значение
10
. То же самое справедливо и в отношении объекта t2
, т.е. в результате создания объекта t2
значение переменной экземпляра t2.x также станет равным
10
. Таким образом, после выполнения этой программы получаем следующий результат:
10 10
146
Часть I. Язык C#
Параметризованные конструкторы
В предыдущем примере использовался конструктор без параметров. Но чаще приходится иметь дело с конструкторами, которые принимают один или несколько параметров. Параметры вносятся в конструктор точно так же, как в метод: для этого достаточно объявить их внутри круглых скобок после имени конструктора. Например, в следующей программе используется параметризованный конструктор.
// Использование параметризованного конструктора. using System; class MyClass { public int x; public MyClass(int i) { x = i;
}
} class ParmConsDemo { public static void Main() {
MyClass t1 = new MyClass(10);
MyClass t2 = new MyClass(88);
Console.WriteLine(t1.x + " " + t2.x);
}
}
Результат выполнения этой программы выглядит так:
10 88
В конструкторе
MyClass() этой версии программы определен один параметр с именем i
, который используется для инициализации переменной экземпляра x
. Таким образом, при выполнении строки кода
MyClass t1 = new MyClass(10); параметру i
передается значение
10
, которое затем присваивается переменной экземпляра x
Добавление конструктора в класс Building
Мы можем улучшить класс Building, добавив в него конструктор, который при создании объекта автоматически инициализирует поля (т.е. переменные экземпляра) floors
, area и occupants
. Обратите особое внимание на то, как создаются объекты класса
Building
// Добавление конструктора в класс Building. using System; class Building { public int floors; // количество этажей public int area; // общая площадь основания здания public int occupants; // количество жильцов public Building(int f, int a, int o) {
Глава 6. Введение в классы, объекты и методы
147 floors = f; area = a; occupants = o;
}
//
Метод возвращает значение площади, которая
// приходится на одного человека. public int areaPerPerson() { return area / occupants;
}
/*
Метод возвращает максимальное возможное количество человек в здании, если на каждого должна приходиться заданная минимальная площадь. */ public int maxOccupant(int minArea) { return area / minArea;
}
}
// Используем параметризованный конструктор Building(). class BuildingDemo { public static void Main() {
Building house = new Building(2, 2500, 4);
Building office = new Building(3, 4200, 25);
Console.WriteLine(
"Максимальное число человек для дома, \n" +
"если на каждого должно приходиться " +
300
+
" квадратных футов: " + house.maxOccupant(300));
Console.WriteLine(
"Максимальное число человек для офиса, \n" +
"если на каждого должно приходиться " +
300
+
" квадратных футов: " + office.maxOccupant(300));
}
}
Результаты выполнения этой программы совпадают с результатами выполнения предыдущей ее версии.
Оба объекта, house и office
, в момент создания инициализируются в программе конструктором
Building()
. Каждый объект инициализируется в соответствии с тем, как заданы параметры, передаваемые конструктору. Например, при выполнении строки
Building house = new Building(2, 2500, 4); конструктору
Building()
передаются значения 2, 2500 и 4 в момент, когда оператор new создает объект класса
Building
. В результате этого копии переменных floors
, area и occupants
, принадлежащие объекту house
, будут содержать значения 2, 2500 и 4, соответственно.
Использование оператора new
Теперь, когда вы больше знаете о классах и их конструкторах, можно подробнее ознакомиться с оператором new
. Формат его таков:
переменная_типа_класса
= new
имя_класса
();
148 Часть I. Язык C#
Здесь элемент
переменная_типа_классаозначает имя создаваемой переменной типа класса. Нетрудно догадаться, что под элементом
имя_классапонимается имя реализуемого в объекте класса. Имя класса вместе со следующей за ним парой круглых скобок — это ни что иное, как конструктор реализуемого класса. Если в классе конструктор не определен явным образом, оператор new будет использовать конструктор по умолчанию, который предоставляется средствами языка C#. Таким образом, оператор new можно использовать для создания объекта любого “классового” типа.
Поскольку объем памяти компьютера ограничен, вероятна ситуация, когда оператор new не сможет выделить область, необходимую для создаваемого объекта, по причине ее отсутствия в достаточном количестве. В этом случае возникнет исключительная ситуация соответствующего типа. (Как обрабатывать эту и другие исключительные ситуации, вы узнаете в главе 13.) Что касается программ, приведенных в этой книге, об “утечке” памяти беспокоиться не стоит, но в собственных программах вы всегда должны учитывать эту возможность.
Применение оператора new к переменным типа значений Вероятно, вас удивил этот заголовок, и вы, возможно, попробовали бы заменить его таким: “Почему не следует применять оператор new к таким переменным типа значений, как int или float
”. В C# переменная типа значения содержит собственное значение. Во время компиляции программы компилятор автоматически выделяет память для хранения этого значения. Следовательно, нет необходимости использовать оператор new для явного выделения памяти, И напротив, в переменных ссылочного типа хранится ссылка на объект, а память для хранения этого объекта выделяется динамически, т.е. во время выполнения программы.
Отсутствие преобразования значений таких фундаментальных типов, как int или char
, в значения ссылочных типов существенно улучшает производительность программы.
При использовании же ссылочных типов существует уровень косвенности, который несет с собой дополнительные затраты системных ресурсов на доступ к каждому объекту. Этих дополнительных затрат нет при использовании типов значений.
Тем не менее вполне допустимо использовать оператор new и с типами значений. Вот пример: int i = new int();
В этом случае вызывается конструктор по умолчанию для типа int
, который инициализирует переменную i
нулем. Рассмотрим следующую программу:
// Использование оператора new с типами значений. using System; class newValue { public static void Main() { int i = new int(); // Инициализация i нулем.
Console.WriteLine("Значение переменной i равно: " + i);
}
}
При выполнении этой программы мы видим следующие результаты:
Значение переменной i равно: 0
Как
подтверждают результаты, переменная i
действительно была установлена равной нулю. Вспомните: без оператора new переменная i
осталась бы неинициализированной, и попытка использовать ее в методе
WriteLine()
без явного присвоения ей конкретного значения привела бы к ошибке.
Глава 6. Введение в классы, объекты и методы
149
В общем случае вызов оператора new для любого нессылочного типа означает вызов конструктора по умолчанию для соответствующего типа. Но в этом случае динамического выделения памяти не происходит. Большинство программистов не используют оператор new с нессылочными типами.
Сбор "мусора" и использование деструкторов
Как упоминалось выше, при использовании оператора new объектам динамически выделяется память из пула свободной памяти. Безусловно, объем буфера динамически выделяемой памяти не бесконечен, и рано или поздно свободная память может исчерпаться.
Следовательно, результат выполнения оператора new может быть неудачным из-за недостатка свободной памяти для создания желаемого объекта. Поэтому одним из ключевых компонентов схемы динамического выделения памяти является восстановление свободной памяти от неиспользуемых объектов, что позволяет делать ее доступной для создания последующих объектов. Во многих языках программирования освобождение ранее выделенной памяти выполняется вручную. Например, в C++ для этого служит оператор delete
. Однако в C# эта проблема решается по-другому, а именно с использованием системы сбора мусора.
Система сбора мусора C# автоматически возвращает память для повторного использования, действуя незаметно и без вмешательства программиста. Ее работа заключается в следующем. Если не существует ни одной ссылки на объект, то предполагается, что этот объект больше не нужен, и занимаемая им память освобождается.
Эту (восстановленную) память снова можно использовать для размещения других объектов.
Система сбора мусора действует только спорадически во время выполнения отдельной программы. Эта система может и бездействовать: она не “включается” лишь потому, что существует один или несколько объектов, которые больше не используются в программе. Поскольку на сбор мусора требуется определенное время, динамическая система C# активизирует этот процесс только по необходимости или в специальных случаях. Таким образом, вы даже не будете знать, когда происходит сбор мусора, а когда — нет.
Деструкторы
Средства языка C# позволяют определить метод, который должен вызываться непосредственно перед тем, как объект будет окончательно разрушен системой сбора мусора. Этот метод называется деструктором, и его можно использовать для обеспечения гарантии “чистоты” ликвидации объекта. Например, вы могли бы использовать деструктор для гарантированного закрытия файла, открытого некоторым объектом.
Формат записи деструктора такой:
имя_класса
() {
// код деструктора
}
Очевидно, что элемент
имя_класса
здесь означает имя класса. Таким образом, деструктор объявляется подобно конструктору за исключением того, что его имени предшествует символ “тильда” (). (Подобно конструктору, деструктор не возвращает значения.)
Чтобы добавить деструктор в класс, достаточно включить его как член. Он вызывается в момент, предшествующий процессу утилизации объекта. В теле деструктора вы указываете действия, которые, по вашему мнению, должны быть выполнены перед разрушением объекта.
150
Часть I. Язык C#
Важно понимать, что деструктор вызывается только перед началом работы системы сбора мусора и не вызывается, например, когда объект выходит за пределы области видимости. (Этим C#-деструкторы отличаются от C++-деструкторов, которые как раз
вызываются, когда объект выходит за пределы области видимости.) Это означает, что вы не можете точно знать, когда будет выполнен деструктор. Однако точно известно, что все деструкторы будут вызваны перед завершением программы.
Использование деструктора демонстрируется в следующей программе, которая создает и разрушает большое количество объектов. В определенный момент выполнения этого процесса будет активизирован сбор мусора, а значит, вызваны деструкторы разрушаемых объектов.
// Демонстрация использования деструктора. using System; class Destruct { public int x; public Destruct(int i) { x = i;
}
//
Вызывается при утилизации объекта.
Destruct()
{
Console.WriteLine("Деструктуризация " + x);
}
//
Метод создает объект, который немедленно // разрушается. public void generator(int i) {
Destruct о = new Destruct(i);
}
} class DestructDemo { public static void Main() { int count;
Destruct ob = new Destruct(0);
/*
Теперь сгенерируем большое число объектов.
В какой-то момент начнется сбор мусора.
Замечание: возможно, для активизации этого процесса вам придется увеличить количество генерируемых объектов. */ for(count = 1; count < 100000; count++) ob.generator(count);
Console.WriteLine("Готово!");
}
}
Вот как работает эта программа. Конструктор устанавливает переменную экземпляра x
равной известному числу. В данном примере x
используется как ID (идентификационный номер) объекта. Деструктор отображает значение переменной x
при утилизации объекта.
Рассмотрим метод generator()
. Он создает объект класса
Destruct
, а затем разрушает его с уведомлением об этом. Класс
DestructDemo создает исходный объект класса
Destruct с именем ob
. Затем, используя объект ob
, он
Глава 6. Введение в классы, объекты и методы
151 создает еще 100 000 объектов, вызывая для него метод generator()
. В различные моменты этого процесса будет активизироваться сбор мусора. Насколько часто и когда именно, —
зависит от таких факторов, как исходный объем свободной памяти, операционная система и пр. Но в некоторый момент времени на экране появится сообщение, сгенерированное деструктором. Если вы не увидите его до завершения программы (т.е. до вывода сообщения "Готово!"), попробуйте увеличить количество генерируемых объектов в цикле for
Из-за недетерминированных условий вызова деструкторы не следует использовать для выполнения действий, которые должны быть привязаны к определенной точке программы. И еще. Существует возможность принудительного выполнения сбора мусора.
Об этом вы прочтете в части II при рассмотрении библиотеки C#-классов. Все же в большинстве случаев процесс сбора мусора инициировать вручную не рекомендуется, так как это может снизить неэффективность работы программы. Кроме того, даже если в явном виде активизировать сбор мусора, то из-за особенностей организации этого процесса все равно не удастся точно узнать, когда утилизирован указанный объект.
Ключевое слово this В заключение стоит представить ключевое слово this
. При. вызове метода ему автоматически передается неявно заданный аргумент, который представляет собой ссылку на вызывающий объект (т.е. объект, для которого вызывается метод). Эта ссылка и называется ключевым словом this
. Чтобы понять смысл ссылки this
, рассмотрим сначала программу, создающую класс
Rect
, который инкапсулирует значения ширины и высоты прямоугольника и включает метод area()
, вычисляющий площадь прямоугольника. using System; class Rect { public int width; public int height; public Rect(int w, int h) { width = w; height = h;
} public int area() { return width * height;
}
} class UseRect { public static void Main() {
Rect r1 = new Rect(4, 5);
Rect r2 = new Rect(7, 9);
Console.WriteLine(
"Площадь прямоугольника r1: " + r1.area());
Console.WriteLine(
"Площадь прямоугольника r2: " + r2.area());
}
}
152
Часть I. Язык C#
Как вам уже известно, внутри метода можно получить прямой доступ к другим членам класса, т.е. без указания имени объекта или класса. Таким образом, внутри метода area()
инструкция return width * height; означает, что будут перемножены копии переменных width и height
, связанные с вызывающим объектом, и метод вернет их произведение. Но та же самая инструкция может быть переписана следующим образом: return this.width * this.height;
Здесь слово this ссылается на объект, для которого вызван метод area()
Следовательно, выражение this.width ссылается на копию переменной width этого объекта, а выражение this.height
— на копию переменной height того же объекта.
Например, если бы метод area()
был вызван для объекта с именем x
, то ссылка this в предыдущей инструкции была бы ссылкой на объект x
. Запись этой инструкции без использования слова this
— это по сути ее сокращенный вариант,
Вот как выглядит полный класс
Rect
, написанный с использованием ссылки this
: using System; class Rect { public int width; public int height; public Rect(int w, int h) { this.width = w; this.height = h;
} public int area() { return this.width * this.height;
}
} class UseRect { public static void Main() {
Rect r1 = new Rect(4, 5);
Rect r2 = new Rect(7, 9);
Console.WriteLine(
"Площадь прямоугольника r1: " + r1.area());
Console.WriteLine(
"Площадь прямоугольника r2: " + r2.area());
}
}
В действительности ни один C#-программист не использует ссылку this так, как показано в этой программе, поскольку это не дает никакого выигрыша, да и стандартная форма выглядит проще. Однако из this можно иногда извлечь пользу. Например, синтаксис C# допускает, чтобы имя параметра или локальной переменной совпадало с именем переменной экземпляра. В этом случае локальное имя будет скрывать переменную экземпляра. И тогда доступ к скрытой переменной экземпляра можно получить с помощью ссылки this
. Например, следующий фрагмент кода (хотя его стиль написания не рекомендуется к применению) представляет собой синтаксически допустимый способ определения конструктора
Rect()
Глава 6. Введение в классы, объекты и методы
153 public Rect(int width, int height) { this.width = width; this.height = height;
}
В этой версии конструктора имена параметров совпадают с именами переменных экземпляра, в результате чего за первыми скрываются вторые, а ключевое слово this как раз и используется для доступа к скрытым переменным экземпляра.