Простой класс
В языке C# класс может быть очень простым:
class VerySimple { int simpleValue = 0; }
class Test { public static void Main() { VerySimple vs = new VerySimple(); } }
Этот класс содержит одну целочисленную переменную. Эта переменная
объявлена без спецификатора доступа, поэтому она является закрытой
(private), т.е. ее нельзя использовать за пределами класса VerySimple.
При создании закрытых переменных можно также указывать модификатор private
явным образом.
Целое число simpleValue является членом класса. Можно объявлять члены
класса различных типов. В функции Main() система создает экземпляр
класса в куче (heap memory), обнуляет все данные класса и возвращает
ссылку на созданный экземпляр класса.
Если экземпляр класса перестает быть нужным, нет необходимости
указывать это. В приведенном примере при завершении функции Main()
ссылка на экземпляр класса перестает существовать. Если данная ссылка не
была сохранена в другом месте, экземпляр класса становится доступным для
сборщика мусора. Сборщик мусора освободит память, выделенную под
экземпляр класса, когда это будет необходимо.
Это все очень хорошо, но этот класс не делает ничего полезного,
поскольку его поле является закрытым. Приведем более полезный пример:
using System; class Point { // constructor public Point(int x, int y) { this.x = x; this.y = y; } // member fields public int x; public int y; }
class Test { public static void Main() { Point myPoint = new Point(10, 15); Console.WriteLine("myPoint.x {0}", myPoint.x); Console.WriteLine("myPoint.y {0}", myPoint.y); } }
В этом примере создается класс Point, в котором есть два целочисленных
поля x и y. Эти поля объявлены открытыми (public), т.е. к ним может
обращаться любой код, который использует класс Point. В классе также
имеется конструктор. Конструктором называется специальная функция,
которая вызывается при создании экземпляра класса. В приведенном примере
конструктор принимает два целочисленных параметра. В этом конструкторе
используется специальная переменная this. Переменная this
доступна в пределах любой функции-члена и всегда является ссылкой на
текущий экземпляр класса.
В функциях-членах локальные переменные и параметры могут закрывать поля
класса, если они имеют такое же имя. Когда необходимо обратиться к полю
класса, имеющему то же имя, что и параметр функции, следует использовать
следующий синтаксис: this.<name>
В конструкторе из приведенного примера x является ссылкой на
параметр конструктора, а this.x позволяет обратиться к полю x.
В программе также присутствует класс Test, который содержит функцию
Main(), вызываемую при запуске программы. Функция Main() создает
экземпляр класса Point, при этом выделяя память под объект, и затем
вызывает конструктор класса. Конструктор устанавливает значения полей x
и y.
Остальные строки функции Main() выводят на экран значения x и y.
Функции-члены (member
functions)
Конструктор из предыдущей программы является примером функции-члена,
т.е. части кода, которую можно вызвать для экземпляра класса.
Конструкторы вызываются автоматически, когда экземпляр класса создается
с помощью ключевого слова new.
Другие функции-члены можно объявить следующим образом:
using System; class Point { public Point(int x, int y) { this.x = x; this.y = y; } // accessor functions public int GetX() {return(x);} public int GetY() {return(y);} // variables now private int x; int y; }
class Test { public static void Main() { Point myPoint = new Point(10, 15); Console.WriteLine("myPoint.X {0}", myPoint.GetX()); Console.WriteLine("myPoint.Y {0}", myPoint.GetY()); } }
В этом примере происходит непосредственное обращение к полям. Обычно
это плохая идея, т.к. это означает, что использование класса зависит от
имен его полей. Это накладывает ограничения на возможные будущие
модификации.
В C# для обращения к закрытым полям чаще используются свойства, а не
функции-члены. Свойства более удобны в использовании, т.к. скрывают от
пользователя информацию о поле класса.
Параметры с модификаторами
ref и out
Вызывать две функции-члена, чтобы получить два значения, не всегда
удобно, поэтому было бы хорошо иметь возможность получить оба значения с
помощью только одного вызова функции. Однако функция может возвращать
только одно значение. Решение этой проблемы заключается в том, чтобы
использовать ссылочный (или ref) параметр. При этом значения параметров,
передаваемых функции, могут быть изменены:
// error using System; class Point { public Point(int x, int y) { this.x = x; this.y = y; } // get both values in one function call public void GetPoint(ref int x, ref int y) { x = this.x; y = this.y; } int x; int y; }
class Test { public static void Main() { Point myPoint = new Point(10, 15); int x; int y; // illegal myPoint.GetPoint(ref x, ref y); Console.WriteLine("myPoint({0}, {1})", x, y); } }
В этом примере параметры были объявлены с использованием ключевого
слова ref, и таким же образом они используются при вызове
функции. Этот код должен работать, но при компиляции будет выдано
сообщение об ошибке, в котором будет сказано, что используются
неинициализированные ссылочные параметры x и y. Это означает, что
переменные передаются функции без предварительного присваивания им
каких-либо значений. Компилятор не позволит использовать
неинициализированные переменные.
Существует два варианта решения этой проблемы. Первый способ
заключается в том, чтобы инициализировать переменные сразу во время их
объявления:
class Test { public static void Main() { Point myPoint = new Point(10, 15); int x = 0; int y = 0; myPoint.GetPoint(ref x, ref y); Console.WriteLine("myPoint({0}, {1})", x, y); } }
Теперь код будет скомпилирован, но здесь переменным присваиваются
начальные значения, только для того, чтобы они были изменены функцией
GetPoint(). В C# чаще применяется другой способ определения функции
GetPoint() - с использованием ключевого слова out вместо ref:
using System; class Point { public Point(int x, int y) { this.x = x; this.y = y; } public void GetPoint(out int x, out int y) { x = this.x; y = this.y; } int x; int y; }
class Test { public static void Main() { Point myPoint = new Point(10, 15); int x; int y; myPoint.GetPoint(out x, out y); Console.WriteLine("myPoint({0}, {1})", x, y); } }
Параметры, объявленные с ключевым словом out, ведут себя точно
так же, как и ref-параметры за исключением того, что в качестве out-параметров
можно передавать неинициализированные переменные. Более того, внутри
метода out-параметр в любом случае будет рассматриваться как не
имеющий начального значения. Метод, получающий out-параметр,
обязан присвоить ему значение до того, как передаст управление
вызывающей функции.
Перегрузка (Overloading)
Часто бывает удобно иметь две функции, которые выполняют одну и ту же
работу, но принимают разные параметры. Это особенно часто применимо по
отношению к конструкторам, если существует несколько способов создания
объекта класса.
class Point { // create a new point from x and y values public Point(int x, int y) { this.x = x; this.y = y; } // create a point from an existing point public Point(Point p) { this.x = p.x; this.y = p.y; } int x; int y; }
class Test { public static void Main() { Point myPoint = new Point(10, 15); Point mySecondPoint = new Point(myPoint); } }
Здесь класс имеет два конструктора: один вызывается с двумя параметрами
x и y, а второй принимает в качестве параметра ссылку на другой объект
класса Point. В функции Main() используются оба конструктора.
При вызове перегруженной функции компилятор выбирает подходящую функцию
путем сравнения параметров, используемых при вызове и при объявлении
функции.