Методы
ГЛАВА 6
Методы
Как вы узнали из главы 1, классы — это инкапсулированные наборы данных и методов, обрабатывающих эти данные. Иначе говоря, методы определяют поведение классов. Мы называем методы в соответствии с действиями, выполняемыми классами по нашему желанию в наших интересах. До сих пор я не вдавался в подробности определения и вызова методов на С#. Вот этому и будет посвящена данная глава: здесь вы узнаете о ключевых словах параметров методов re/и out, а также как с их помощью определяются методы, которые возвращают вызывающему коду больше одного значения. Вы также научитесь определять перегруженные методы — когда несколько методов с одинаковым именем могут по-разному функционировать в зависимости от типов и/или числа переданных им аргументов. После этого вы узнаете, как поступать в ситуациях, когда до момента выполнения неизвестно точное число аргументов метода. В завершение мы рассмотрим виртуальные методы на основе обсуждения наследования (см. главу 5) и способов определения статических методов.
При попытке получения информации с помощью метода на С# вы получите только возвращаемое значение. Поэтому может показаться, что в результате вызова метода вы получите не более одного значения. Очевидно, что отдельно вызывать метод для каждой порции данных во многих ситуациях будет очень неуклюжим решением. Допустим, у вас есть класс Color, представляющий любой цвет в виде трех значений согласно стандарту RGB (красный-зеленый-синий). Использование лишь возвращаемых значений вынудит вас написать следующий код, чтобы получить все три значения:
Предполагаем, что color - экземпляр класса Color, int red = color. GetRedQ; int green = color.GetGreenO; int blue = color. GetBlueQ;
Но нам хочется получить что-то вроде этого:
int red;
int green;
int blue;
color.GetRGB(red,green,blue);
Но при этом возникает проблема. При вызове метода color.GetRGB значения аргументов red, green и blue копируются в локальный стек метода, а переменные вызывающей функции остаются без изменений, сделанных методом.
На C++ эта проблема решается путем передачи при вызове метода указателей или ссылок на эти переменные, что позволяет методу обрабатывать данные вызывающей функции. Решение на С# выглядит аналогично. На самом деле С# предлагает два похожих решения. Первое из них использует ключевое слово ref. Оно сообщает компилятору С#, что передаваемые аргументы указывают на ту же область памяти, что и переменные вызывающего кода. Таким образом, если вызванный метод изменяет их и возвращает управление, переменные вызывающего кода также подвергнутся изменениям. Следующий код иллюстрирует использование ключевого слова ref на примере класса Color.
using System;
class Color {
public Color() {
this.red = 255; this.green = 0; this.blue = 125; }
protected int red; protected int green; protected int blue;
public void GetColors(ref int red, ref int green, ref int blue) {
red = this.red; green = this.green; blue = this.blue; > }
class RefTestlApp {
public static void Main()
{
Color color = new ColorQ;
int red;
int green;
int blue;
color.GetColors(ref red, ref green, ref blue);
Console.WriteLine("red = {0}, green = {1}. blue = {2}", red, green, blue);
} }
У ключевого слова re/есть один недостаток, а приведенный код фактически не будет компилироваться. При использовании ключевого слова re/перед вызовом метода вы должны инициализировать передаваемые аргументы. Поэтому, чтобы этот код заработал, его нужно изменить:
using System;
class Color {
public ColorQ {
this.red = 255; this.green = 0; this.blue = 125; }
protected int red; protected int green; protected int blue;
public void GetColors(ref int red, ref int green, ref int blue)
red = this.red;
green = this.green; blue = this.blue; } }
class RefTest2App {
public static void Main() {
Color color = new ColorO; int red = 0; int green = 0; int blue = 0;
color.GetColors(ref red, ref green, ref blue);
Console. WriteLineC 1 red = {0}, green = {1}, blue = {2}", red, green, blue);
}
He кажется ли вам, что инициализация переменных, которые позже будут перезаписаны, бессмысленна? Поэтому С# предоставляет альтернативный способ передачи аргументов, изменения значений которых должны быть видимыми вызывающему коду: с помощью ключевого слова out. Вот пример с тем же классом Color, где используется out
using System;
class Color {
public Color() {
this.red = 255; this.green = 0; this.blue = 125; }
protected int red;
protected int green;
protected int blue;
public void GetColors(out int red, out int green, out int blue) <
red = this.red;
green = this, green; blue = this.blue; } >
class OutTestlApp {
public static void Main() {
Color color = new ColorO; int red; int green; int blue;
color.6etColors(out red, out green, out blue);
Console.WriteLine("red = {0}, green = {1}, blue = {2}",
red, green, blue); } }
Единственное различие между ключевыми словами ref и out в том, что out не требует, чтобы вызывающий код сначала инициализировал передаваемые аргументы. А когда же применять ref! Когда нужна гарантия, что вызывающий код инициализировал аргумент. В приведенных примерах можно было применять ключевое слово out, так как вызываемый метод не зависит от значения передаваемой переменной. Ну а если вызываемый метод использует значение параметра? Взгляните:
using System;
class Window {
public Window(int x, int y) {
this.x = x; this.у = у; }
protected int x; protected int y;
public void Move(int x, int y) {
this.x = x; this,у = у; }
public void ChangePos(ref int x, ref int y) {
this.x += x;;
this.у += у;
x = this.x; у = this.у; } }
class OutTest2App {
public static void Main()
{
Window wnd = new Window(5, 5);
int x = 5; int у = 5;
wnd.ChangePos(ref x, ref y);
Console.WriteLine("{0}, {1}", x, y);
x = -1; У = -1;
wnd.ChangePos(ref x, ref y);
Console.WriteLine("{0}, {1}", x, y); } >
Как видите, работа вызываемого метода Window.Change Pos основана на переданных ему значениях. В данном случае ключевое слово ref вынуждает вызывающий код инициализировать эти значения, чтобы метод работал корректно.
Перегрузка методов позволяет программистам на С# многократно использовать одни и те же имена методов, меняя лишь передаваемые аргументы. Это очень полезно по крайней мере в двух сценариях. Первый:
вам нужно иметь единое имя метода, поведение которого немного различается в зависимости от типа переданных аргументов. Допустим, у вас есть класс, отвечающий за протоколирование, позволяющий вашему приложению записывать на диск диагностическую информацию. Чтобы немного повысить гибкость класса, вы можете создать несколько форм метода Write, определяющих тип записываемой информации. Кроме собственно строки, подлежащей записи, метод также может принимать строку идентификатора ресурса. Без перегрузки методов вам пришлось бы реализовать отдельные методы наподобие WriteString и WriteFrom-Resourceld для каждой ситуации. Однако перегрузка методов позволяет реализовать оба эти метода под именем WriteEntry, они будут различаться лишь типом параметра:
using System;
class Log
{
public Log(string fileName)
{
// Открыть файл fileName и перейти в его конец.
}
public void WriteEntry(string entry)
{
Console.WriteLine(entry);
}
public void WriteEntry(int resourceld)
{
Console.WriteLine ("Получение строки на основе идентификатора ресурса и запись
в журнал"); } }
class OverloadlngtApp
{
public static void Main()
{
Log log = new LogC'Mou файл");
log.WriteEntry("3anncb № 1");
log.WriteEntry(42); } }
Второй сценарий, в котором выгодно применять перегрузку метода, — использование конструкторов, которые в сущности представляют собой методы, вызываемые при создании экземпляра объекта. Допустим, вы хотите создать класс, который может быть построен несколькими способами. Например, он использует описатель (int) или имя (string) файла, чтобы открыть его. Поскольку правила С# диктуют, что у конструктора класса должно быть такое же имя, как и у самого класса, вы не можете просто создать разные методы для переменных каждого типа. Вместо этого нужно использовать перегрузку конструктора: using System;
class File
{
>
class CommaDeliinitedFile
{
public CommaDelimitedFile(String fileName) {
Console.WriteLine("Конструктор, использующий имя файла");
}
public CommaDelimitedFile(File file) {
Console.WriteLineC"Конструктор, использующий файловый объект"); }
}
class Overloading2App {
public static void HainQ {
File file = new File();
ConmaDelimitedFile flle2 = new ConnaDelinitedFile(file);
ConnaDelinitedFile flleS = new ConmaDelinitedFileCMiM некоторого файла");
}
О перегрузке метода важно помнить следующее: список аргументов каждого метода должен отличаться. Поэтому следующий код не будет компилироваться, так как единственное различие между двумя версиями OverloadingSApp.Foo — тип возвращаемого значения:
using System;
class OverloadingSApp <
void Foo(double input)
{
Console.WriteLine("Overloading3App.Foo(double)");
// ОШИБКА: методы отличаются лишь типом возвращаемого значения.
// Код компилироваться не будет, double Foo(double input)
{
Console.WriteLine("Overloading3App.Foo(double)
(вторая версия)"); }
public static void Main()
{
OverloadingSApp app = new Overloading3App();
double i = 5; app.Foo(l);
}
Иногда число аргументов, которые будут переданы методу, неизвестно до периода выполнения. Например, вам нужен класс, который чертит на графике линию, заданную последовательностями х- и у-координат. У вас может быть класс с методом, принимающим единственный аргумент — объект Point, который представляет значения обеих координат х и у. Затем этот метод сохраняет каждый объект Point в связанном списке или в элементах массива, пока вызывающая программа не даст команду на вывод всей последовательности точек. Однако это решение не годится по двум причинам. Во-первых, оно требует от пользователя рутинной работы по вызову одного и того же метода для каждой точки линии, которую нужно нарисовать, — очень утомительно, если линия будет длиной; а чтобы нарисовать линию, затем приходится вызывать другой метод. Во-вторых, данное решение требует от класса хранить эти точки, тогда как они нужны лишь для того, чтобы использовать единственный метод — DrawLine. Эту проблему эффективно решает переменное количество аргументов.
Вы можете задавать переменное число параметров метода через ключевое слово params и указывая массив в списке аргументов метода. Вот пример класса Draw, написанного на С#, позволяющего пользователю одним вызовом получить и вывести произвольное число объектов Point:
using System;
class Point {
public Point(int x, int y) {
this.x = x; this.у = у; }
public int x; public int y; }
class Chart {
public void DrawLine(params Point[] p) {
Console.WriteLine("\пЭтот метод позволяет нарисовать линию " + "по следующий точкам:");
for (int i = 0; i < p.GetLength(O); i++) {
Console.WriteLine("{0), {1}". p[i].x, p[i].y); } } }
class VarArgsApp {
public static void Main() {
Point p1 = new Point(5,10); Point p2 = new Point(5, 15);
Point p3 = new Point(5, 20);
Chart chart = new ChartQ;
chart.DrawLine(p1, p2, p3); } }
Метод DrawLine сообщает компилятору С#, что он может принимать переменное число объектов Point. Затем в период выполнения метод ис- \ пользует простой цикл for для прохода по всем объектам Point и вывода всех точек.
В реальном приложении для доступа к членам х и у объекта Point на-, много лучше будет использовать свойства, чем объявлять эти члены как public. Кроме того, в методе DrawLine было бы лучше применять оператop foreach вместо цикла for. Но чтобы не нарушать ход изложения ма-т.ериала, я пока не буду вводить эти функции языка. О свойствах я расскажу в главе 7, а об операторе foreach — в главе И.
Как вы узнали из главы 5, вы можете производить один класс из Другого, при этом новый класс может наследовать возможности уже существующего класса. Так как тогда мы еще ничего не знали о методах, мы\лишь вскользь коснулись наследования полей и методов. Иначе говоря, мы еще не рассматривали возможности изменения поведения производных классов. А делается это с помощью виртуальных методов.
Давайте сначала рассмотрим способы подмены (override) функциональности базового класса в унаследованном методе. Начнем с базового класса, представляющего сотрудника. Чтобы максимально упростить пример, у этого класса будет единственный метод — CalculatePay, который будет сообщать имя вызываемого метода и ничего более. Позднее это поможет нам определить, какие методы дерева наследования вызываются.
class Employee {
public void CalculatePayO
{
Console. WriteHne(" Employee. CalculatePay()");
} }
А теперь допустим, что вы хотите создать класс, производный от Employee, и подменить метод CalculatePay, чтобы выполнять какие-либо действия, специфичные для производного класса. Для этого вам понадобится ключевое слово new с определением метода производного класса. Вот как это делается:
using System; class Employee
public void CalculatePayOK(Console. WriteLineC'Employee. CalculatePayO"); I
}
} I
class SalariedEmployee : Employee ' {
// Ключевое слово new позволяет заменить
// реализацию, содержащуюся в базовом классе.
new public void CalculatePayO
{
Console.Writeline("SalariedEmployee.CalculatePayC)");
} }
class PolylApp {
public static void MainO
{
PolylApp polyl = new Poly1App();
Employee baseE = new EmployeeO;
baseE.CalculatePayC);
SalariedEmployee s = new SalariedEmployeeO;
s. CalculatePayO; } >
Скомпилировав и запустив это приложение, вы получите такую информацию:
c:\>Poly1App
Employee.CalculatePay()
Salaried. CalculatePay()
Подмена методов с помощью ключевого слова new замечательно работает, если у вас есть ссылка на производный объект. А что будет, если у вас есть ссылка, приведенная к базовому классу, но нужно, чтобы ком- \ пилятор вызывал реализацию метода из производного класса? Это имен- \ но тот момент, когда в дело вступает полиморфизм. Полиморфизм позволяет вам многократно определять методы в иерархии классов так, что в период выполнения в зависимости от того, какой именно объект используется, вызывается соответствующая версия данного метода. \ Обратимся к нашему примеру с сотрудником. Приложение PolylApp работает корректно, поскольку у нас есть два объекта: Employee и Sala-rfedEmployee. В более практичном приложении мы, вероятно, считыва-лИ бы все записи о сотрудниках из БД и заполняли бы ими массив. Хотя некоторые сотрудники работают по контракту, а другим выплачивается зарплата, все они должны быть помещены в массив в виде объектов одного типа — базового класса Employee. Но при циклической обработке массива, при которой происходит получение каждого объекта и вызов его метода CalculatePay, нам бы хотелось, чтобы компилятор вызывал подходящую реализацию метода CalculatePay. \
В следующем примере я добавил новый класс — ContractEmployee. Главный класс приложения теперь содержит массив объектов типа Employee и два дополнительных метода: LoadEmployees, загружающий объекты «сотрудник» в массив, и DoPayroll, обрабатывающий массив, вызывая метод CalculatePay для каждого объекта.
using System;
class Employee {
public Employee(string name)
{
this.Name = name;
protected string Name;
public string name {
get
{
return this.Name;
}
public void CalculatePayO {
Console.WriteLine("Employee.CalculatePay вызван " + "для {0}", name);
} }
class ContractEmployee : Employee {
public ContractEmployee(string name)
: base(name)
{
}
public new void CalculatePayO {
Console.WriteLine("ContractEmployee.CalculatePay вызван "
"для {0}", name); } }
class SalariedEmployee : Employee {
public SalariedEmployee (string name)
: base(name)
{
}
public new void CalculatePayO {
Console.WriteLine("SalariedEmployee.CalculatePay вызван " н
"для {О}", name); }
class Poly2App {
protected Employee[] employees; public void LoadEmployeesO {
// Имитация загрузки информации из базы данных,
employees = new Employee[2];
employees[0] = new ContractEmployee("Kate Dresen");
employees[1] = new SalariedEmployee("Megan Sherman"); }
public void DoPayrollO {
foreach(Employee emp in employees)
I <
\ emp. CalculatePayO;
I public static void Main()
\ < \ Poly2App poly2 = new Poly2App();
I poly2. LoadEmployeesO; \ poly2. DoPayrollO;
' } }
Однако, запустив это приложение, вы получите такую информацию:
c:\>Poly2App
Employee.CalculatePay вызван для Kate Dresen
Employee.CalculatePay вызван для Megan Sherman
Ясно, что это совсем не то, что нам нужно: для каждого объекта Bfy-зывается реализация метода CalculatePay из базового класса. То, что здесь происходит, — пример раннего связывания (early binding). Во время компиляции этого кода компилятор С#, изучив вызов emp.CalculatePay, определил адрес памяти, на который ему нужно перейти во время этого вызова. В этом случае это будет адрес метода Employee.CalculatePay.
Взгляните на следующий MSIL-код, сгенерированный из приложения Ро1у2Арр, и обратите особое внимание на строку IL_0014 и тот факт, что в ней производится явный вызов метода Employ ее.Calculate Pay.
.method public hidebysig instance void DoPayrollO il managed {
// Размер кода 34 (0x22) .maxstack 2
.locals (class Employee V_0, class Employee[] V_1, int32 V_2, int32 V_3) ILJJOOO: ldarg.0
IL_0001: Idfld class Employee[] Poly2App::employees
IL_0006: stloc.1
IL_0007: ldloc.1
IL_0008: Idlen
IL_0009: conv.14
IL.OOOa: stloo.2
ILJJOOb: ldo.14.0
IL_OOOc: stloc.3 I
ILJWOd: br.s IL_001d
IL_OOOf: ldloc.1 /
IL_0010: ldloc.3 /
IL_0011: Idelem.ref /
IL_0012: stloc.O /
IL_0013: ldloc.0 /
IL_0014: call instance void Employee::CalculatePay() /
IL_0019: ldloc.3 /
IL_001a: ldc.14.1 !
IL_001b: add
IL_001c: stloc.3
IL_001d: ldloc.3
IL_001e: ldloc.2
IL_001f: blt.s ILJWOf
IL_0021: ret
} // конец метода Poly2App::DoPayroll
Этот вызов метода Employee.CalculatePay представляет собой проблему. Мы хотим, чтобы вместо раннего связывания происходило динамическое (позднее) связывание (late binding). Динамическое связывание означает, что компилятор не выбирает метод для исполнения до периода выполнения. Чтобы заставить компилятор выбрать правильную версию метода объекта мы используем два новых ключевых слова: virtual и over-rids. С методом базового класса применяется ключевое слово virtual, a override — с реализацией метода в производном классе. Вот еще один пример, который на этот раз работает должным образом!
using System;
class Employee {
public Employee(string name)
{
this.Name = name;
>
protected string Name; public string name {
get
{
return this.Name;
\ virtual public void CalculatePayO
\ <
I Console.WriteLine("Employee.CalculatePay вызван " +
\ "для {О}", name);
} }
class ContractEmployee : Employee {
public ContractEmployee(string name)
: base(name)
{
>
override public void CalculatePayO
{
Console.WriteLine("ContractEmployee.CalculatePay вызван " *' "для {0}", name);
} >
class SalariedEmployee : Employee <
public SalariedEmployee (string name)
: base(name)
{
}
override public void CalculatePayO
<
Console.WriteLine("SalariedEmployee.CalculatePay вызван " + "для {0}", name);
} }
class PolySApp
{
protected Employee[] employees; public void LoadEmployeesO
// Имитация загрузки информации из базы данных. /
employees = new Employee[2]; I
employees[0] = new ContractEmployee("Kate Dresen"); /
employees[1] = new SalariedEmployee("Megan Sherman"); /
> /
public void DoPayrolK) /
{ /' foreach(Employee emp in employees) i
{ ' / emp.CalculatePayO;
} }
public static void Main()
{
Poly3App poly3 = new Poly3App();
poly3. LoadEmployeesO;
poly3. DoPayrolK); } }
Перед запуском этого приложения взглянем на сгенерированный для него IL-код. Заметьте, что на этот раз в строке ILJ)014 используется код операции MSIL callvirt, который сообщает компилятору, что до периода выполнения неизвестно, какой именно метод будет вызван, так как он зависит от вида используемого производного объекта.
.method public hidebysig instance void DoPayrolK) il managed
{
// размер кода 34 (0x22) .maxstack 2 .locals (class Employee V_0,
class Employee[] V_1,
int32 V_2,
int32 v_3) ILJJOOO: ldarg.0
IL_0001: Idfld class Employee[] PolySApp::employees Il__0006: stloc.1 IL_0007: ldloc.1
IL_0008: Idlen
IL_0009: conv.i4
IL_OOOa: stloc.2
ILJWOb: Idc.i4.0
IL_OOOc: stloc.3
IL.OOOd: br.s IL_001d
IL_OOOf: ldloc.1
IL_0010: ldloc.3
IL_0011: Idelem.ref
IL_0012: stloc.O
IL_0013: ldloc.0
IL_0014: callvirt instance void Employee::CalculatePay()
IL.0019: ldloc.3
IL_001a: Idc.i4.1
IL_001b: add
IL_001c: stloc.3
IL_001d: ldloc.3
IL_001e: ldloc.2
IL_001f: blt.S IL_OOOf
IL.0021: ret
// конец метода PolySApp;:DoPayroll
Выполнение такого коды приведет к следующим результатам:
с:\>Ро!уЗАрр
ContractEmployee.CalculatePay вызван для Kate Dresen
SalariedEmployee.CalculatePay вызван для Megan Sherman
ПРИМЕЧАНИЕ
Виртуальные методы нельзя объявлять как закрытые, поскольку они по определению не будут видимы в производных классах.
Статическим называется метод, который существует в классе как в таковом, а не в отдельных его экземплярах. Как и в случае других статических членов, главное преимущество статических методов в том, что они расположены вне конкретных экземпляров класса, не засоряя глобальное пространство приложения. При этом они и не нарушают принципов ООП, поскольку ассоциированы с определенным классом. Примером может служить API баз данных, написанный мной на С#. В моей иерархии классов есть класс SQLServerDb. Помимо базовых операций для работы с БД (new, update, read и delete), класс содержит метод, предназкаченный для восстановления БД. В методе Repair не нужно открывать саму БД. Я использую функцию ODBC (SQLConfigDataSource), которая предполагает, что БД закрыта. Однако, конструктор SQLServerDb открыл БД, указанную переданным ему именем. Поэтому здесь очень удобно использовать статический метод. Это позволило мне поместить метод в класс SQLServerDb, к которому он принадлежит, и даже не обращаться к конструктору моего класса. Очевидно, выгода клиента в том, что он также не должен создавать экземпляр класса SQLServerDb. В следующем примере вы можете видеть вызов статического метода (RepairDatabast) из метода Main. Заметьте, что для этого не создается экземпляр SQLServerDB:
using System;
class SQLServerDb
( { // Набор остальных обычных членов.
public static void RepairDatabaseQ <
Console.WriteLine("восстановление БД..."); > }
class StaticMethodlApp {
public static void Main()
<
SQLServerDb. RepairDatabaseO; > }
Определить метод как статический позволяет ключевое слово static. Затем для вызова метода пользователь применяет синтаксис вида Класс. Метод. Этот синтаксис необходим, даже если у пользователя есть ссылка на экземпляр класса. Этот момент можно проиллюстрировать кодом, который не будет компилироваться:
//Этот код компилироваться не будет, using System;
class SQLServerDb {
// Набор остальных обычных членов.
public static void RepairDatabaseO
{
Console.WriteLine("восстановление БД..."); } }
class StaticMethod2App {
public static void Hain() {
SQLServerDb db = new SQLServerDbO; db. RepairDatabaseO; } }
Последним моментом, касающимся статических методов, является правило, определяющее, к каким членам класса можно обращаться из статического метода. Как вы можете догадаться, статический метод может обращаться любому статическому члену в пределах класса, но не может обращаться к члену экземпляра. Например:
using System;
class SQLServerDb {
static string progressStringl = " восстановление БД...";
string progressString2 = " восстановление БД...";
public static void RepairDatabaseO {
Console.WriteLine(progressStringl); // Это будет работать.
Console.WriteLine(progressString2); // Компиляция не пройдет. } }
class StaticMethodSApp {
public static void Main()
{
SQLServerDb. RepairDatabaseO;
} }
Подведем итоги
Методы определяют поведение классов и выполняют необходимые нам действия. Методы С# могут возвращать несколько значений, допускают перегрузку и позволяют задавать переменное число параметров. Ключевые слова refvi out позволяют методу возвращать более одного значения. Благодаря перегрузке несколько одноименных методов могут функционировать по-разному в зависимости типа и/или числа переданных им аргументов. Число параметров метода может быть переменным. Ключевое слово params позволяет иметь дело с методами, число параметров которых неизвестно до периода выполнения. Виртуальные методы позволяют вам изменять методы в унаследованных классах. Наконец, ключевое слово static позволяет методам существовать как часть класса, а не как часть объекта.