Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Объектно-ориентированное программирование.-6

.pdf
Скачиваний:
6
Добавлен:
05.02.2023
Размер:
4.5 Mб
Скачать

<фактический параметр> :: <выражение> <фактический параметр> :: ref <l-value> <фактический параметр> :: out <l-value>

Тип фактического параметра должен совпадать с типом соответствующего формального параметра. Также должны быть указаны модификаторы ref или out, если они были указаны у формального параметра.

Два разных метода могут иметь одинаковые имена, если:

1.Они различаются списком формальных аргументов;

2.Один из методов явно реализует метод интерфейса. Пример:

interface IMyInterface

{

void Method();

}

partial class MyClass : IMyInterface

{

public void Method()

{

Console.WriteLine("Method");

}

void IMyInterface.Method()

{

Console.WriteLine("IMyInterface.Method");

}

private void Method(int x) { } private void Method() { } // Ошибка

partial void Part();

partial void Part()

{

Console.WriteLine("Part");

}

}

class Program

{

static int Main()

{

MyClass cls = new MyClass(); cls.Method(); // Method

((IMyInterface)cls).Method(); // IMyInterface.Method return 0;

}

}

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

271

ние и не имеет модификаторов доступа (т.е. всегда является private).

Пример: Samples\4.4\4_4_1_methods.

4.4.1.1.Виды формальных параметров

Вязыке C++ предусмотрена передача параметров по значению, по ссылке и по указателю. В первом случае в методе создается локальная копия параметра, и все изменения локального параметра никак не отражаются на исходном значении:

void f1(int i)

{

i = 5;

}

void main(void)

{

int x = 1; f(x); // x = 1

}

В результате переменная «x» сохранит свое значение 1. При передаче параметра по ссылке или по указателю в метод передается адрес переменной, поэтому при изменении локального параметра меняется и исходное значение:

void f2(int &i)

{

i = 5;

}

void f3(int *i)

{

*i = 5;

}

void main(void)

{

int y = 1; int z = 1;

f2(y); // y = 5 f3(&z); // z = 5

}

При этом требуется, чтобы у параметра можно было вычислить адрес (т.е. чтобы оно являлось l-value). Например, следующие вызовы приведут к ошибке компиляции:

f2(5); // Ошибка f2(z + y); // Ошибка

В языке C# существует схожее решение. Без указания модификаторов

272

формальный параметр считается входным. В метод передается его копия. Чтобы сделать параметр выходным (т.е. чтобы через него можно было вернуть выходные результаты работы метода), его нужно объявить с модификатором ref или out. В этом случае передается ссылка на параметр. Разница между этими модификаторов в том, что ref требует обязательной инициализации параметра перед использованием. А модификатор out требует обязательной инициализации параметра в вызываемом методе до передачи управления в вызывающий метод. Выходные параметры, как и в языке C++, обязательно должны являться l-value:

static void MetI(int i) { i = 5; } static void MetR(ref int i) { i = 7; } static void MetO(out int i) { i = 9; } static void MetWAR(ref int i) { } // ОК

static void MetWAO(out int i) { } // Ошибка

static int Main()

{

int i = 1; int j;

MetI(i);

Console.WriteLine(i); // 1

MetR(ref i);

Console.WriteLine(i); // 7

MetO(out i);

Console.WriteLine(i); // 9

MetI(j); // Ошибка

MetR(ref j); // Ошибка

MetO(out j); // ОК

MetO(out 5); // Ошибка MetO(out i + j); // Ошибка return 0;

}

Считается, что сигнатура параметра с модификатором ref или out отличается от сигнатуры параметра без модификаторов, но сигнатура ref не отличается от сигнатуры out:

void F(int i) { }

void F(ref int i) { } // ОК void F(out int i) { } // Ошибка

4.4.1.2. Методы расширения

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

273

щихся универсальными или вложенными. Комбинировать модификатор this с другими модификаторами нельзя. При следующем объявлении метода расширения

static <тип результата> <имя метода> (this <тип> <идентификатор> [, <список формальных параметров>])

в результате мы расширяем тип <тип> новым методом экземпляра

<тип результата> <тип>.<имя метода> ([<список формальных параметров>])

Пример:

static class ExtMethod

{

public static string X(this string s)

{

return "ExtMethod.X";

}

}

class Program

{

static int Main()

{

string s = String.Empty;

Console.WriteLine(s.X()); // ExtMethod.X return 0;

}

}

Вданном примере мы в класс String добавили новый метод X().

4.4.1.3. Переменное число параметров метода

Вязыке C++ существует возможность описывать методы с переменным числом параметров, используя многоточие, например:

void f(int count, ...);

Были предусмотрены специальные средства для работы с такими параметрами. В языке C# все проще: т.к. все типы данных имеют единый корень System.Object, любое количество параметров можно представить в виде массива object[] или массива производных от Object классов – массива параметров. Как и в языке C++, в списке параметров может быть только один массив параметров, и он должен быть последним в списке. Для описания массива параметров используется модификатор params:

static void Write() { Console.WriteLine("()"); }

static void Write(int i, int j) { Console.WriteLine("(int,int)"); } static void Write(params int[] i) { Console.WriteLine("(int[])"); }

274

static int Main()

 

 

{

 

 

Write();

// ()

Write(1);

//

(int[])

Write(1, 1);

//

(int,int)

Write(1, 1, 1); // (int[]) return 0;

}

Видно, что компилятор сам подбирает метод с наиболее подходящей сигнатурой. Комбинировать params с другими модификаторами нельзя. Тип данных в массиве параметров может быть любой, но он обязательно должен быть массивом, причем прямоугольные массивы использовать нельзя:

static void Write(params object[][] obj) { } // ОК static void Write(params string[,] str) { } // Ошибка

4.4.1.4. Передача структуры и ссылки класса в метод

Все, сказанное про входные и выходные параметры, верно для типов данных по значению. А что насчет классов? Ведь они и так являются ссылочными типами, следовательно, в метод будет в любом случае передаваться ссылка на экземпляр класса:

static void Met(MyClass2 cls) { cls.a = 5; } static void Met(ref MyClass2 cls) { cls.a = 7; } static void Met(MyStruct2 str) { str.a = 5; } static void Met(ref MyStruct2 str) { str.a = 7; }

static int Main()

{

MyClass2 cls2 = new MyClass2(); MyStruct2 str2 = new MyStruct2();

Met(cls2);

Met(str2); Console.WriteLine(cls2.a); // 5 Console.WriteLine(str2.a); // 0 Met(ref cls2);

Met(ref str2); Console.WriteLine(cls2.a); // 7 Console.WriteLine(str2.a); // 7 return 0;

}

Видим, что в данном случае для класса нет никакой разницы, указан модификатор ref или нет, а поведение структуры отличается. На самом деле, разница есть, но только если модифицировать не содержимое класса, а ссылку на класс. Т.е. ссылка тоже может быть передана как входной и как выходной параметр:

275

static void MetR(MyClass2 cls) { cls = null; } static void MetR(ref MyClass2 cls) { cls = null; }

static int Main()

{

MyClass2 cls2 = new MyClass2(); MetR(cls2);

Console.WriteLine(cls2 == null); // False MetR(ref cls2);

Console.WriteLine(cls2 == null); // True return 0;

}

Посмотрим на рис. 4.6. В первом случае в параметр cls в методе MetR копировалась ссылка на экземпляр класса, лежащий в динамической куче. Поэтому мы, меняя поле класса cls.a, меняли тем самым и поле класса cls2.a. Но, обнулив ссылку cls, мы не повлияли на cls2. Во втором же случае cls неявно является ссылкой на cls2. Поэтому, когда мы пишем cls = null, тем самым мы обнуляем ссылку cls2.

 

Стек

 

 

Куча

 

 

 

 

 

 

1)

cls2

 

 

a

cls

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Стек

 

Куча

 

 

 

 

 

2)

 

cls2

 

a

 

 

 

cls

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 4.6 – Передача ссылки по значению и по ссылке

В языке C++ тоже существует такая возможность – указатели в методы можно передавать не только по значению, но также по ссылке и по указателю.

4.4.2. Конструкторы

Конструкторы – это специальные методы, вызывающиеся в момент создания экземпляра класса. Экземпляры класса создаются при помощи оператора new, всегда имеющего определенный тип возвращаемого значения:

276

<экземпляр класса> operator new <класс>([<параметры конструктора>])

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

В языке C++ существует две возможности создать экземпляр класса – на стеке и в динамической куче:

MyClass cls1; // на стеке без параметров

MyClass cls2(<параметры>); // на стеке с параметрами MyClass cls3 = cls1; // на стеке копированием

MyClass *cls4 = new MyClass; // в куче без параметров

MyClass *cls5 = new MyClass(<параметры>); // в куче с параметрами

Причем конструкторы в языке C++ делятся на три группы, имеющие существенные особенности – конструкторы по умолчанию, конструкторы копирования и все прочие конструкторы с параметрами. Из-за того, что в языке C# копирование объектов было серьезно переосмыслено, конструкторы копирования потеряли свою значимость. Вариант №3 теперь просто копирует ссылку, а остальные варианты вызовут ошибку компиляции. Поэтому

вязыке C# различают три вида конструкторов:

конструкторы по умолчанию,

остальные конструкторы экземпляров,

и статические конструкторы.

Хотя ссылка является, по сути, указателем, это скрыто от программиста. Нет никакой возможности (в управляемом коде) получить реальный адрес объекта. Поэтому, в отличие от синтаксиса C++, символ указателя опускается, хотя выделение памяти происходит похожим образом:

MyClass cls1 = new MyClass(); // в куче без параметров

MyClass cls2 = new MyClass(<параметры>); // в куче с параметрами

MyClass cls3 = cls1; // копирование ссылки

Как и другие методы, конструктор может быть полиморфным, т.е. иметь несколько реализаций, различающихся формальными аргументами. Синтаксис:

<конструктор> :: [<атрибуты>] [<модификаторы>] [static] <имя класса> ([<список формальных параметров>]) [<инициализатор>]

<тело конструктора>

<тело конструктора> :: <блок> <тело конструктора> :: ;

<инициализатор> :: : base ([<список фактических параметров>]) <инициализатор> :: : this ([<список фактических параметров>])

Как и в языке C++, имя конструктора совпадает с именем класса. Тело

277

может отсутствовать у внешнего (extern) конструктора.

Пример: Samples\4.4\4_4_2_constr.

4.4.2.1. Конструкторы по умолчанию

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

class MyClass1

{

}

class MyClass2

{

public MyClass2() { Console.WriteLine("MyClass2()"); }

public MyClass2(int i) { Console.WriteLine("MyClass2(int)"); }

}

class MyClass3

{

public MyClass3(int i) { Console.WriteLine("MyClass3(int)"); }

}

struct MyStruct

{

public MyStruct() { } // Ошибка

public MyStruct(int i) { Console.WriteLine("MyStruct(int)"); }

}

static int Main()

{

MyClass1 cls1 = new MyClass1(); MyClass2 cls21 = new MyClass2(); MyClass2 cls22 = new MyClass2(1);

MyClass3 cls31 = new MyClass3(); // Ошибка MyClass3 cls32 = new MyClass3(1);

MyStruct str1 = new MyStruct(); // ОК MyStruct str2 = new MyStruct(1);

return 0;

}

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

4.4.2.2. Инициализаторы конструкторов

Перед тем, как выполнится конструктор экземпляра, можно выполнить

278

его инициализатор. Инициализаторы бывают двух видов:

Вида base(...), который активизирует конструктор экземпляра базового класса (см. п. 4.6.2);

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

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

Пример:

class MyClass4

{

public MyClass4() { Console.WriteLine("MyClass4()"); } public MyClass4(int i) : this() {

Console.WriteLine("MyClass4(int)"); }

public MyClass4(int i, int j) : this(1) { Console.WriteLine("MyClass4(int,int)"); }

public MyClass4(char c) : this(c) { } // Ошибка

}

4.4.2.3. Статические конструкторы

Конструкторы также могут быть статическими. Они предназначены для инициализации статических полей класса, если это требуется. Если конструктор статический, то он один для всех экземпляров класса. Явно его вызывать нельзя, CLR делает это автоматически. Причем предсказать момент вызова статического конструктора невозможно. Однако гарантируется, что это произойдет до момента использования класса (вызова его статического метода, создания экземпляра и т.п.). Пример:

class MyClass5

 

{

 

static MyClass5() {

Console.WriteLine("static MyClass5"); }

public MyClass5() {

Console.WriteLine("public MyClass5"); }

}

 

class MyClass6

 

{

 

static MyClass6(int

q) { } // Ошибка

static MyClass6() {

Console.WriteLine("static MyClass6"); }

}

 

static int Main()

 

{

 

MyClass5 cls5 = new

MyClass5(); // ОК

 

 

 

279

MyClass6 cls6 = new MyClass6(); // ОК

return 0;

}

В данном случае строка «static MyClass» всегда будет появляться на консоли раньше, чем «public MyClass». Сигнатура статического конструктора может совпадать с сигнатурой конструктора по умолчанию, это не считается ошибкой. Соответственно, если в классе нет других конструкторов, кроме статического, то неявно добавляется конструктор по умолчанию.

Статические конструкторы не могут иметь модификаторов доступа и параметров. Статический конструктор статического класса никогда не будет вызван.

4.4.3. Деструкторы

Деструктор – это метод, который вызывается при уничтожении объекта. Так как в .NET CLR используется сборщик мусора, нельзя явно удалить определенный экземпляр. Удаление происходит, когда никакой код уже не может воспользоваться этим экземпляром (т.е. когда не остается ссылок на данный экземпляр).

Синтаксис:

<деструктор> :: [<атрибуты>] ~<имя класса> () <тело деструктора>

<тело деструктора> :: <блок> <тело деструктора> :: ;

Имя деструктора состоит из тильды и имени класса. Параметры отсутствуют, модификаторы доступа не указываются. Как уже было сказано в пункте 4.2.2, для класса, содержащего деструктор, зарезервирована сигнатура «void Finalize()». Если деструктор внешний (extern), то его тело в классе отсутствует.

Статические классы и структуры не могут содержать деструкторы.

4.4.4. Метод Main

Укаждого приложения на C# должен быть метод Main, определенный

водном из его классов. Кроме того, этот метод должен быть определен как public (хотя в последних версиях .NET Framework это не обязательно) и static. Для компилятора C# не важно, в каком из классов определен метод Main, а класс, выбранный для этого, не влияет на порядок компиляции.

280