Статические методы и методы экземпляра
Статический метод объявляется с помощью модификатора static. Если модификатор static отсутствует, метод называется методом экземпляра.
Статический метод не выполняет операций с конкретным экземпляром. При использовании зарезервированного слова this в статическом методе возникает ошибка компиляции.
Метод экземпляра выполняет операции с конкретным экземпляром класса, обращение к которому осуществляется с помощью зарезервированного слова this (§7.6.7).
При доступе к члену (§7.6.4) с использованием ссылки на метод в форме E.M для статического метода M параметр E должен обозначать тип, содержащий M, а для метода экземпляра M E должен обозначать экземпляр типа, содержащего M.
Различия между статическими членами и членами экземпляров рассматриваются в разделе §10.3.7.
Виртуальные методы
Если объявление метода экземпляра содержит модификатор virtual, метод является виртуальным методом. Если модификатор virtual отсутствует, метод называется невиртуальным методом.
Реализация невиртуального метода инвариантна и одинакова вне зависимости от того, вызывается ли метод для экземпляра класса, в котором он объявлен, или для экземпляра производного класса. В отличие от этого, реализация виртуального метода может быть заменена производными классами. Процесс замены реализации унаследованного виртуального метода называется переопределением этого метода (§10.6.4).
При вызове виртуального метода тип времени выполнения экземпляра, для которого осуществляется вызов, определяет фактическую реализацию вызываемого метода. При вызове невиртуального метода определяющим фактором является тип времени компиляции экземпляра. Говоря точнее, если метод N вызывается со списком аргументов A в экземпляре с типом времени компиляции C и с типом времени выполнения R (где R может быть C или экземпляром класса, производного от C), вызов обрабатывается следующим образом.
· Сначала к C, N и A применяется разрешение перегрузки для выбора конкретного метода M из набора методов, объявленных и унаследованных классом C. Это описано в §7.6.5.1.
· Затем, если M является невиртуальным методом, вызывается метод M.
· В противном случае M является виртуальным методом, и вызывается старшая производная реализация метода M по отношению к R.
Для каждого виртуального метода, объявленного в классе или унаследованного им, существует старшая производная реализация метода по отношению к этому классу. Старшая производная реализация виртуального метода M по отношению к классу R определяется следующим образом.
· Если R содержит представляющее объявление virtual метода M, это является старшей производной реализацией M.
· В противном случае, если R содержит объявление override метода M, это является старшей производной реализацией M.
· В противном случае старшей производной реализацией метода M по отношению к R является старшая производная реализация M по отношению к прямому базовому классу R.
В следующем примере показаны различия между виртуальными и невиртуальными методами.
using System;
class A
{
public void F() { Console.WriteLine("A.F"); }
public virtual void G() { Console.WriteLine("A.G"); }
}
class B: A
{
new public void F() { Console.WriteLine("B.F"); }
public override void G() { Console.WriteLine("B.G"); }
}
class Test
{
static void Main() {
B b = new B();
A a = b;
a.F();
b.F();
a.G();
b.G();
}
}
В этом примере класс A представляет невиртуальный метод F и виртуальный метод G. Класс B представляет новый невиртуальный метод F, скрывая тем самым унаследованную функцию F, а также переопределяет унаследованный метод G. Далее показан вывод для вышеуказанного примера.
A.F
B.F
B.G
B.G
Обратите внимание, что оператор a.G() вызывает метод B.G, а не A.G. Это связано с тем, что фактически вызываемая реализация метода определяется типом времени выполнения (B), а не типом времени компиляции экземпляра (A).
Поскольку методам разрешено скрывать унаследованные методы, класс может содержать несколько виртуальных методов с одной и той же сигнатурой. При этом не возникает проблема неоднозначности, поскольку видимым является только старший производный метод. В этом примере
using System;
class A
{
public virtual void F() { Console.WriteLine("A.F"); }
}
class B: A
{
public override void F() { Console.WriteLine("B.F"); }
}
class C: B
{
new public virtual void F() { Console.WriteLine("C.F"); }
}
class D: C
{
public override void F() { Console.WriteLine("D.F"); }
}
class Test
{
static void Main() {
D d = new D();
A a = d;
B b = d;
C c = d;
a.F();
b.F();
c.F();
d.F();
}
}
классы C и D содержат два виртуальных метода с одинаковыми подписями, один из которых представлен классом A а второй — классом C. Метод, представляемый классом C, скрывает метод, наследуемый от A. Таким образом, объявление переопределения в классе D переопределяет метод, представленный классом C. При этом в классе D невозможно переопределить метод, представленный классом A. Далее показан вывод для вышеуказанного примера.
B.F
B.F
D.F
D.F
Обратите внимание, что можно вызвать скрытый виртуальный метод посредством обращения к экземпляру класса D с помощью младшего производного типа, в котором этот метод не скрыт.
Переопределяющие методы
Если объявление метода экземпляра содержит модификатор override, метод является переопределяющим методом. Такой метод переопределяет унаследованный виртуальный метод с такой же сигнатурой. Объявление виртуального метода представляет новый метод. Объявление переопределяющего метода уточняет существующий виртуальный метод, предоставляя его новую реализацию.
Метод, переопределяемый с помощью объявления override, называется переопределенным базовым методом. Для переопределяющего метода M, объявленного в классе C, переопределенный базовый метод определяется посредством проверки всех базовых для C типов класса. Проверка начинается с прямого базового типа класса C и продолжается для каждого последующего прямого базового типа класса. Проверка продолжается до тех пор, пока не будет найден базовый тип класса, содержащий хотя бы один доступный метод M, подпись которого после замены аргументов типа соответствует заданной. Переопределенный базовый метод считается доступным, если он объявлен как public, protected, protected internal или объявлен как internal в той же программе, что и C.
Если для объявления переопределения не выполняются все следующие условия, возникает ошибка времени компиляции.
· Может быть найден переопределенный базовый метод (см. выше).
· Существует только один переопределенный базовый метод. Это ограничение влияет только в том случае, если тип базового класса представляет собой сформированный тип, в котором сигнатуры двух методов после замены аргументов типа совпадают.
· Переопределенный базовый является виртуальным, абстрактным или переопределяющим методом. Другими словами, переопределенный базовый метод не может быть статическим или невиртуальным.
· Переопределенный базовый метод не является запечатанным методом.
· Переопределяющий метод и переопределенный базовый метод имеют одинаковые типы возвращаемых данных.
· Для объявления переопределения и переопределенного базового метода объявлен одинаковый уровень доступа. Другими словами, объявление переопределения не может изменять доступность виртуального метода. Однако если переопределенный базовый метод объявлен как protected internal в сборке, отличной от той, в которой содержится переопределяющий метод, для последнего следует объявить уровень доступа protected.
· Объявление переопределения не содержит предложений_ограничений_параметров_типа. Все ограничения наследуются из переопределенного базового метода. Обратите внимание, что ограничения, являющиеся параметрами типа в переопределенном методе могут быть заменены аргументами типа в унаследованном ограничении. Это может привести к ограничениям, не являющимся законными при явном задании, таким как типы значений или запечатанные типы.
В следующем примере показано применение правил переопределения для универсальных классов.
abstract class C<T>
{
public virtual T F() {...}
public virtual C<T> G() {...}
public virtual void H(C<T> x) {...}
}
class D: C<string>
{
public override string F() {...} // Ok
public override C<string> G() {...} // Ok
public override void H(C<T> x) {...} // Error, should be C<string>
}
class E<T,U>: C<U>
{
public override U F() {...} // Ok
public override C<U> G() {...} // Ok
public override void H(C<T> x) {...} // Error, should be C<U>
}
Объявление переопределения может иметь доступ к переопределенному базовому методу с помощью доступа base (§7.6.8). В этом примере
class A
{
int x;
public virtual void PrintFields() {
Console.WriteLine("x = {0}", x);
}
}
class B: A
{
int y;
public override void PrintFields() {
base.PrintFields();
Console.WriteLine("y = {0}", y);
}
}
с помощью метода base.PrintFields() класса B вызывается метод PrintFields, объявленный в классе A. Доступ base отключает механизм виртуального вызова и рассматривает базовый метод просто как невиртуальный метод. Если записать вызов в классе B как ((A)this).PrintFields(), это приведет к рекурсивному вызову метода PrintFields, объявленного в классе B, а не в классе A, поскольку метод PrintFields является виртуальным, а типом времени выполнения для ((A)this) является B.
Переопределение другого метода возможно только посредством включения модификатора override. Во всех других случаях метод с сигнатурой, совпадающей с сигнатурой унаследованного метода, просто скрывает унаследованный метод. В этом примере
class A
{
public virtual void F() {}
}
class B: A
{
public virtual void F() {} // Warning, hiding inherited F()
}
метод F класса B не содержит модификатор override и, следовательно, не переопределяет метод F класса A. В этом случае метод F класса B скрывает метод класса A, и отображается предупреждение, поскольку объявление не содержит модификатор new.
В этом примере
class A
{
public virtual void F() {}
}
class B: A
{
new private void F() {} // Hides A.F within body of B
}
class C: B
{
public override void F() {} // Ok, overrides A.F
}
метод F класса B скрывает виртуальный метод F, унаследованный из класса A. Поскольку для нового метода F класса B объявлен уровень доступа private, область его действия распространяется только на тело класса B и не включает класс C. Таким образом, объявление метода F в классе C может переопределять метод F унаследованный из класса A.
Запечатанные методы
Если объявление метода экземпляра содержит модификатор sealed, метод является запечатанным методом. Если объявление метода экземпляра включает модификатор sealed, оно должно также включать модификатор override. Использование модификатора sealed предотвращает последующее переопределение метода в производном классе.
Пример:
using System;
class A
{
public virtual void F() {
Console.WriteLine("A.F");
}
public virtual void G() {
Console.WriteLine("A.G");
}
}
class B: A
{
sealed override public void F() {
Console.WriteLine("B.F");
}
override public void G() {
Console.WriteLine("B.G");
}
}
class C: B
{
override public void G() {
Console.WriteLine("C.G");
}
}
класс B предоставляет два переопределяющих метода: метод F с модификатором sealed и метод G без него. Использование модификатораmodifier sealed в классе B препятствует дальнейшему переопределению метода F в классе C.
Абстрактные методы
Если объявление метода экземпляра содержит модификатор abstract, метод является абстрактным методом. Хотя абстрактный метод неявно является также виртуальным методом, он не может иметь модификатора virtual.
Объявление абстрактного метода представляет новый виртуальный метод, но не предоставляет его реализацию. Вместо этого необходимо предоставить неабстрактные производные классы, предоставляющие собственные реализации этого метода посредством его переопределения. Поскольку абстрактный метод не предоставляет фактическую реализацию, тело абстрактного метода состоит только из точки с запятой.
Объявления абстрактных методов допускаются только в абстрактных классах (§10.1.1.1).
В этом примере
public abstract class Shape
{
public abstract void Paint(Graphics g, Rectangle r);
}
public class Ellipse: Shape
{
public override void Paint(Graphics g, Rectangle r) {
g.DrawEllipse(r);
}
}
public class Box: Shape
{
public override void Paint(Graphics g, Rectangle r) {
g.DrawRect(r);
}
}
класс Shape определяет абстрактное представление геометрической фигуры самоокрашивающегося объекта. Метод Paint является абстрактным, поскольку отсутствует значащая реализация по умолчанию. Классы Ellipse и Box представляют собой конкретные реализации класса Shape. Поскольку эти классы являются неабстрактными, требуется, чтобы они переопределяли метод Paint и предоставляли фактическую реализацию.
Если доступ base (§7.6.8) ссылается на абстрактный метод, возникает ошибка времени компиляции. В этом примере
abstract class A
{
public abstract void F();
}
class B: A
{
public override void F() {
base.F(); // Error, base.F is abstract
}
}
при вызове метода base.F() выполняется ссылка на абстрактный метод, что порождает ошибку времени компиляции.
В объявлении абстрактного метода допускается переопределение виртуального метода. Это позволяет абстрактному классу принудительно выполнить повторную реализацию метода в производных классах. При этом исходная реализация метода становится недоступна. В этом примере
using System;
class A
{
public virtual void F() {
Console.WriteLine("A.F");
}
}
abstract class B: A
{
public abstract override void F();
}
class C: B
{
public override void F() {
Console.WriteLine("C.F");
}
}
в классе A объявляется виртуальный метод, который переопределяется в классе B абстрактным методом. Последний переопределяется в классе C с использованием собственной реализации.
Внешние методы
Если объявление метода содержит модификатор extern, метод является внешним методом. Внешние методы обычно реализуются внешне с помощью языков, отличных от C#. Вследствие того, что объявление внешнего метода предоставляет фактическую реализацию, тело метода внешнего метода состоит просто из точки с запятой. Внешний метод не может быть универсальным.
Модификатор extern обычно используется совместно с атрибутом DllImport (§17.5.1), что позволяет реализовывать внешние методы с помощью библиотек динамической компоновки (DLL). Среда выполнения может поддерживать другие механизмы реализации внешних методов.
Если внешний метод содержит атрибут DllImport, объявление метода также должно включать модификатор static. Этот пример демонстрирует использование модификатора extern и атрибута DllImport.
using System.Text;
using System.Security.Permissions;
using System.Runtime.InteropServices;
class Path
{
[DllImport("kernel32", SetLastError=true)]
static extern bool CreateDirectory(string name, SecurityAttribute sa);
[DllImport("kernel32", SetLastError=true)]
static extern bool RemoveDirectory(string name);
[DllImport("kernel32", SetLastError=true)]
static extern int GetCurrentDirectory(int bufSize, StringBuilder buf);
[DllImport("kernel32", SetLastError=true)]
static extern bool SetCurrentDirectory(string name);
}
Разделяемые методы
Если объявление метода содержит модификатор partial, метод является разделяемым методом. Разделяемые методы могут объявляться только в качестве членов разделяемых типов (§10.2). На использование таких методов накладывается ряд ограничений. Дополнительные сведения о разделяемых методах см. в §10.2.7.
Методы расширения
Если первый параметр метода содержит модификатор this, такой метод называется методом расширения. Методы расширения могут быть объявлены только в статических классах, не являющихся универсальными или вложенными. Первый параметр метода расширения не может содержать отличных от this модификаторов или иметь тип указателя.
В следующем примере показан статический класс, в котором объявляются два метода расширения.
public static class Extensions
{
public static int ToInt32(this string s) {
return Int32.Parse(s);
}
public static T[] Slice<T>(this T[] source, int index, int count) {
if (index < 0 || count < 0 || source.Length – index < count)
throw new ArgumentException();
T[] result = new T[count];
Array.Copy(source, index, result, 0, count);
return result;
}
}
Метод расширения представляет собой регулярный статический метод. Кроме того, в области действия включающего его статистического класса метод расширения может быть вызван с использованием синтаксиса вызова метода экземпляра (§7.6.5.2). В этом случае в качестве первого аргумента используется выражение-получатель.
В следующей программе используются объявленные выше методы расширения.
static class Program
{
static void Main() {
string[] strings = { "1", "22", "333", "4444" };
foreach (string s in strings.Slice(1, 2)) {
Console.WriteLine(s.ToInt32());
}
}
}
Метод Slice доступен для string[], а метод ToInt32 — для string, поскольку оба они объявлены как методы расширения. Эта программа аналогична следующей, в которой используются вызовы обычных статических методов.
static class Program
{
static void Main() {
string[] strings = { "1", "22", "333", "4444" };
foreach (string s in Extensions.Slice(strings, 1, 2)) {
Console.WriteLine(Extensions.ToInt32(s));
}
}
}
Тело метода
Тело метода содержит блок или точку с запятой.
Объявления абстрактных и внешних методов не предоставляют реализацию метода, поэтому их тела содержат только точку с запятой. Тело любого другого метода содержит блок (§8.2), включающий в себя операторы, которые выполняются при вызове метода.
Метод имеет тип результата void, если типом возвращаемого значения является void или, в случае асинхронного метода, типом возвращаемого значения является System.Threading.Tasks.Task. В противном случае типом результата синхронного метода является его тип возвращаемого значения, а типом результата асинхронного метода с типом возвращаемого значения System.Threading.Tasks.Task<T> — T.
Если типом результата метода является void, операторам return (§8.9.4) в теле этого метода не разрешено задавать выражение. Если выполнение метода, возвращающего void, завершается нормально (управление передается из конечной точки тела метода), этот метод просто возвращается его текущему вызывающему объекту.
Если тип результата метода отличен от void, каждый оператор return в теле этого метода должен задавать выражение, неявно преобразуемое к типу результата. Конечная точка тела метода, возвращающего значение, должна быть недостижима. Другими словами, в методе, возвращающем значение, не допускается передача управления из конечной точки тела метода.
В этом примере
class A
{
public int F() {} // Error, return value required
public int G() {
return 1;
}
public int H(bool b) {
if (b) {
return 1;
}
else {
return 0;
}
}
}
метод F, возвращающий значение, порождает ошибку времени компиляции, поскольку в этом случае управление может быть передано из конечной точки тела метода. Методы G и H являются правильными, поскольку в них все возможные ветви выполнения заканчиваются оператором return, задающим возвращаемое значение.
Перегрузка метода
Правила разрешения перегрузки метода описываются в разделе §7.5.2.
Свойства
Свойство — это член, предоставляющий доступ к характеристикам объекта или класса. Примеры свойств: размер шрифта, заголовок окна, имя клиента и т. д. Свойства — это естественное расширение полей. Как свойства, так и поля являются именованными членами со связанными типами, для обращения к которым используется одинаковый синтаксис. Однако в отличие от полей, свойства не указывают места хранения. Вместо этого свойства содержат методы доступа, определяющие операторы, которые используются при чтении или записи их значений. Таким образом, свойства предоставляют механизмы, позволяющие связать определенные действия с чтением или записью атрибутов объекта. Кроме того, свойства обеспечивают вычисление таких атрибутов.
Свойства объявляются с помощью объявлений свойств:
property-declaration:
attributesopt property-modifiersopt type member-name { accessor-declarations }
property-modifiers:
property-modifier
property-modifiers property-modifier
property-modifier:
new
public
protected
internal
private
static
virtual
sealed
override
abstract
extern
member-name:
identifier
interface-type . identifier
Объявление свойства может включать набор атрибутов (§17) допустимое сочетание любых из четырех модификаторов доступа (§10.3.5), а также модификаторы new (§10.3.4), static (§10.6.2), virtual (§10.6.3), override (§10.6.4), sealed (§10.6.5), abstract (§10.6.6) и extern (§10.6.7).
В отношении использования сочетаний модификаторов объявления свойств подчиняются тем же правилам, что и объявления методов (§10.6).
Тип свойства задает представленный в объявлении тип свойства. Имя члена задает имя свойства. Если в свойстве не содержится явная реализация члена интерфейса, имя члена представляет собой просто идентификатор. При явной реализации члена интерфейса (§13.4.1) имя члена состоит из типа интерфейса, точки "." и идентификатора (в указанной последовательности).
Тип свойства должен быть доступен, по меньшей мере, в такой же степени, как и само свойство (§3.5.4).
Объявления методов доступа заключаются в фигурные скобки "{" и "}" и объявляют методы доступа (§10.7.2) свойства. Методы доступа задают исполняемые операторы, связанные с чтением или записью свойства.
Несмотря на то что для обращения к свойствам и полям используется одинаковый синтаксис, свойство не классифицируется как переменная. Поэтому невозможно передать свойство как аргумент ref или out.
Если объявление свойства включает модификатор extern, свойство называется внешним свойством. Поскольку объявление внешнего свойства не предоставляет фактической реализации, каждое из его объявлений методов доступа состоит из точки с запятой.