Защищенный доступ для членов экземпляров
При доступе к члену экземпляра protected вне текста программы, в котором он объявлен, и при доступе к члену экземпляра protected internal вне текста программы, в котором он объявлен, доступ должен осуществляться в объявлении класса, производного от класса, в котором он объявлен. Дополнительно доступ должен осуществляться через экземпляр производного типа класса или типа класса сконструированного из него. Данное ограничение предотвращает доступ одного производного класса к закрытым членам другого производного класса, даже если члены унаследованы от одного базового класса.
Пусть B будет классом, в котором объявлен защищенный член экземпляра M, и пусть D будет производным классом B. В рамках тела класса D доступ к M может принять одну из следующих форм.
· Неполное имя типа или первичное выражение формы M.
· Первичное выражение формы E.M с условием, что тип E является T или производным от T классом, где T является типом класса D или типом класса, сконструированным из D.
· Основное выражение формы base.M.
Дополнительно к данным формам доступа производный класс может осуществлять доступ к конструктору защищенных экземпляров базового класса в инициализаторе конструктора (§10.11.1).
В этом примере
public class A
{
protected int x;
static void F(A a, B b) {
a.x = 1; // Ok
b.x = 1; // Ok
}
}
public class B: A
{
static void F(A a, B b) {
a.x = 1; // Error, must access through instance of B
b.x = 1; // Ok
}
}
в рамках A можно осуществить доступ к x через экземпляры A и B, так как в любом случае доступ осуществляется через экземпляр A или производный от A класс. Однако в рамках B невозможно осуществить доступ к x через экземпляр A, так как A не произведен из B.
В этом примере
class C<T>
{
protected T x;
}
class D<T>: C<T>
{
static void F() {
D<T> dt = new D<T>();
D<int> di = new D<int>();
D<string> ds = new D<string>();
dt.x = default(T);
di.x = 123;
ds.x = "test";
}
}
три присваивания x разрешены, так как они все осуществляются через экземпляры типов класса, сконструированные из универсального типа.
Ограничения доступности
Некоторым конструкциям языка C# требуется, чтобы тип был хотя бы доступен как член или другой тип. Считается, что доступность типа T не меньше доступности члена или типа M, если домен доступности T является множеством домена доступности M. Другими словами, доступность T не меньше доступности M, если T доступен во всех контекстах, в которых доступен M.
Существуют следующие ограничения доступности.
· Прямой базовый класс типа класса должен быть не менее доступен, чем сам тип класса.
· Явный базовый интерфейс типа интерфейса должен быть не менее доступен, чем сам тип интерфейса.
· Тип возвращаемого значения и типы параметров типа делегата должны быть не менее доступны, чем сам тип делегата.
· Тип константы должен быть не менее доступен, чем сама константа.
· Тип поля должен быть не менее доступен, чем само поле.
· Тип возвращаемого значения и типы параметров метода должны быть не менее доступны, чем сам метод.
· Тип свойства должен быть не менее доступен, чем само свойство.
· Тип события должен быть не менее доступен, чем само событие.
· Тип и типы параметра индексатора должны быть не менее доступны, чем сам индексатор.
· Тип возвращаемого значения и типы параметра оператора должны быть не менее доступны, чем сам оператор.
· Типы параметра конструктора экземпляров должны быть не менее доступны, чем сам конструктор экземпляров.
В этом примере
class A {...}
public class B: A {...}
класс B приводит к ошибке времени компиляции, так как A менее доступен, чем B.
Аналогично, в примере
class A {...}
public class B
{
A F() {...}
internal A G() {...}
public A H() {...}
}
метод H в B приводит к ошибке времени компиляции, так как тип возвращаемого значения A менее доступен, чем метод.
Сигнатуры и перегрузка
Методы, конструкторы экземпляров, индексаторы и операторы характеризуются их сигнатурами.
· Сигнатура метода состоит из имени метода, числа параметров типа и типа и вида (значение, ссылка или выход) каждого ее параметра, учтенного в порядке слева направо. Для данных целей каждый параметр типа метода, возникающий в типе формального параметра, идентифицируется не по имени, а по порядковому номеру в списке аргументов типа метода. Сигнатура метода специально не содержит ни тип возвращаемого значения, ни модификатор params, который может быть указан для самого правого параметра, и не содержит дополнительных ограничений параметра типа.
· Сигнатура конструктора экземпляра состоит из типа и вида (значение, ссылка или выход) каждого из формальных параметров, учтенного в порядке слева направо. Сигнатура конструктора экземпляра специально не содержит модификатор params, который может быть указан для самого правого параметра.
· Сигнатура индексатора состоит из типа каждого его формального параметра, учтенного в порядке слева направо. Сигнатура индексатора специально не содержит тип элемента и не содержит модификатор params, который может быть указан для самого правого параметра.
· Сигнатура оператора состоит из имени оператора и типа каждого его формального параметра, учтенного в порядке слева направо. Сигнатура оператора специально не содержит тип результата.
Сигнатуры обеспечивают использование механизма перегрузки членов в классах, структурах и интерфейсах.
· Перегрузка методов позволяет классу, структуре или интерфейсу объявить несколько одноименных методов при условии, что их сигнатуры уникальны в рамках класса, структуры или интерфейса.
· Перегрузка конструкторов экземпляров позволяет классу или структуре объявить несколько конструкторов экземпляров при условии, что их сигнатуры уникальны в рамках класса или структуры.
· Перегрузка индексаторов позволяет классу, структуре или интерфейсу объявить несколько одноименных индексаторов при условии, что их сигнатуры уникальны в рамках класса, структуры или интерфейса.
· Перегрузка операторов позволяет классу или структуре объявить несколько одноименных операторов при условии, что их сигнатуры уникальны в рамках класса или структуры.
Несмотря на то что модификаторы параметров out и ref считаются частью сигнатуры, члены, объявленные в одном типе, не могут отличаться по сигнатуре только параметрами ref и out. Если два члена, объявленные в одном типе, будут иметь одинаковые сигнатуры, в которых все параметры в обоих методах с модификаторами out изменены на модификаторы ref, возникает ошибка времени компиляции. Для других целей сопоставления сигнатур (например, для скрытия или переопределения) ref и out учитываются в качестве части сигнатуры и не соответствуют друг другу. (Данное ограничение позволяет простым способом транслировать программы на C# для выполнения в инфраструктуре Common Language Infrastructure (CLI), не обеспечивающей способ определения методов, отличающихся исключительно по ref и out.)
Для сигнатур типы object и dynamic являются идентичными. Члены, объявленные в одном типе, могут не отличаться в сигнатурах одними лишь object и dynamic.
В следующем примере представлен набор объявлений перегруженных методов с их сигнатурами.
interface ITest
{
void F(); // F()
void F(int x); // F(int)
void F(ref int x); // F(ref int)
void F(out int x); // F(out int) error
void F(int x, int y); // F(int, int)
int F(string s); // F(string)
int F(int x); // F(int) error
void F(string[] a); // F(string[])
void F(params string[] a); // F(string[]) error
}
Обратите внимание, что модификаторы параметров ref и out (§10.6.1) являются частью сигнатуры. Поэтому F(int) и F(ref int) являются уникальными сигнатурами. Однако F(ref int) и F(out int) не могут быть объявлены в одном интерфейсе, так как их сигнатуры отличаются только модификаторами ref и out. Также обратите внимание, что тип возвращаемого значения и модификатор params не являются частью сигнатуры, поэтому невозможно выполнить перегрузку только на основе типа возвращаемого значения или на основе включения или исключения модификатора params. По этим причинам представленные выше объявления методов F(int) и F(params string[]) приводят к ошибке времени компиляции.
Области видимости
Областью видимости имени является часть текста программы, в рамках которой можно ссылаться на сущность, объявленную именем без квалификации имени. Области могут быть вложенными, а внутренняя область может повторно объявлять значение имени из внешней области (однако это не снимает ограничение, налагаемое разделом §3.3, что в рамках вложенного блока невозможно объявить локальную переменную с именем, совпадающим с именем локальной переменной во внешнем блоке). Имя из внешней области при этом считается скрытым в регионе текста программы, покрываемом внутренней областью, а доступ к внешнему имени возможен только по квалифицирующему имени.
· Областью члена пространства имен, объявленного объявлением члена пространства имен (§9.5), без вмещающего объявления пространства имен является полный текст программы.
· Областью члена пространства имен, объявленного объявлением члена пространства имен в рамках объявления пространства имен с полным именем N, является тело пространства имен каждого объявления пространства имен с полным именем N, или начинающимся с N и последующей точкой.
· Область имени, определенного директивой extern alias, распространяется на директивы using, глобальные атрибуты и объявления членов пространства имен незамедлительно содержащегося блока компиляции или тела пространства имен. Директива extern alias не вносит какие-либо новые члены в нижестоящую область объявления. Иначе говоря, директива extern alias не является транзитивной, и влияет только на единицу компиляции или тело пространства имен, в котором находится.
· Область имени, заданная или импортированная с помощью директивы using (§9.4), распространяется на объявления членов пространства имен блока компиляции или тела пространства имен, в котором содержится директива using. Директива using может привести к образованию нуля или более пространств имен или типов имен, доступных в рамках определенного блока компиляции или тела пространства имен, но не может привести к созданию новых членов в нижестоящей области объявления. Другими словами, директива using не является транзитивной, а скорее влияет только на блок компиляции или тело пространства имен, в котором находится.
· Областью параметра типа, объявленного списком параметров типа в объявлении класса (§10.1), является база класса, предложения ограничений параметров типа и тело класса данного объявления класса.
· Областью параметра типа, объявленного списком параметров типа в объявлении структуры (§11.1), является интерфейс структуры, предложения ограничений параметров типа и тело структуры данного объявления структуры.
· Областью параметра типа, объявленного списком параметров типа в объявлении интерфейса (§13.1), является база интерфейса, предложения ограничений параметров типа и тело интерфейса данного объявления интерфейса.
· Областью параметра типа, объявленного списком параметров типа в объявлении делегата (§15.1), является тип возвращаемого значения, список формальных параметров и предложения ограничений параметров типа данного объявления делегата.
· Областью члена, объявленного объявлением члена класса (§10.1.6), является тело класса, в котором сделано объявление. Кроме того, область члена класса распространяется на тела класса производных классов, включенных в домен доступности (§3.5.2) члена.
· Областью члена, объявленного объявлением члена структуры (§11.2), является тело структуры, в котором сделано объявление.
· Областью члена, объявленного объявлением члена перечисляемого типа (§14.3), является тело перечисляемого типа, в котором сделано объявление.
· Областью параметра, объявленного в объявлении метода (§10.6), является тело метода данного объявления метода.
· Областью параметра, объявленного в объявлении индексатора (§10.9), являются объявления метода доступа данного объявления индексатора.
· Областью параметра, объявленного в объявлении оператора (§10.10), является блок данного объявления оператора.
· Областью параметра, объявленного в объявлении конструктора (§10.11), является инициализатор конструктора и блок данного объявления конструктора.
· Областью параметра, объявленного в лямбда-выражении (§7.15), является тело лямбда-выражения данного лямбда-выражения.
· Областью параметра, объявленного в выражении анонимного метода (§7.15), является блок данного выражения анонимного метода.
· Областью метки, объявленной в помеченном операторе (§8.4), является блок, в котором содержится объявление.
· Областью видимости локальной переменной, описанной в объявлении локальной переменной (§8.5.1), является блок, в котором встречается объявление.
· Областью локальной переменной, объявленной в блоке switch оператора switch (§8.7.2), является блок switch.
· Областью локальной переменной, объявленной в инициализаторе for оператора for (§8.8.3), является инициализатор for, условие for, итератор for и содержащий оператор оператора for.
· Областью локальной константы, объявленной в объявлении локальной константы (§8.5.2), является блок, в котором содержится объявление. Ссылка на локальную константу в текстовой позиции, предшествующей декларатору константы, является ошибкой времени компилирования.
· Область переменной, объявленной в качестве части оператора foreach, оператора using, оператора lock или выражения запроса, определяется путем расширения заданной конструкции.
В рамках области члена пространства имен, класса, структуры или перечисляемого типа можно ссылаться на членов в текстовой позиции, предшествующей объявлению члена. Пример
class A
{
void F() {
i = 1;
}
int i = 0;
}
В данном примере ссылка F на i до объявления является верной.
В рамках области локальной переменной ссылка на локальную переменную в текстовой позиции, предшествующей декларатору локальной переменной локальной переменной, будет являться ошибкой времени компилирования. Пример
class A
{
int i = 0;
void F() {
i = 1; // Error, use precedes declaration
int i;
i = 2;
}
void G() {
int j = (j = 1); // Valid
}
void H() {
int a = 1, b = ++a; // Valid
}
}
В вышеуказанном методе F первое присваивание i явно не ссылается на поле, объявленное во внешней области. Однако существует ссылка на локальную переменную, что приводит к ошибке времени компилирования, так как текстуальность предшествует объявлению переменной. В методе G использование j в инициализаторе для объявления j является верным, так как использование не предшествует декларатору локальной переменной. В методе H последующий декларатор локальной переменной верно ссылается на локальную переменную, объявленную в более раннем деклараторе локальной переменной в рамках того же декларатора локальной переменной.
Правила назначения областей для локальных переменных разработаны для того, чтобы значение имени в контексте выражения использовалось только в рамках блока. Если область локальной переменной расширена только из объявления до конца блока, в вышеуказанном примере первое присваивание будет присвоено переменной экземпляра, второе присваивание будет присвоено локальной переменной, что, возможно, приведет к ошибкам времени компилирования, если операторы блока будут затем переупорядочены.
Значение имени в рамках блока может отличаться в зависимости от контекста, в котором используется имя. В этом примере
using System;
class A {}
class Test
{
static void Main() {
string A = "hello, world";
string s = A; // expression context
Type t = typeof(A); // type context
Console.WriteLine(s); // writes "hello, world"
Console.WriteLine(t); // writes "A"
}
}
имя A используется в контексте выражения для ссылки на локальную переменную A и в контексте типа для ссылки на класс A.
Скрытие имени.
Область сущности обычно заключает больше текста программы, чем область объявления сущности. В частности, область сущности может включать объявления, представляющие новые области объявлений, содержащие сущности с одинаковыми именами. Такие объявления приводят к тому, что исходная сущность становится скрытой. И наоборот, сущность считается видимой, если она не скрыта.
Скрытие имени происходит, когда области перекрываются через вложение и когда области перекрываются через наследование. Характеристики двух типов скрытия описываются в следующих разделах.
Скрытие через вложение.
Скрытие имени через вложение возникает в результате вложения пространств имен или типов в рамках пространств имен, в результате вложения типов в рамках классов или структур и в результате объявлений параметров и локальных переменных.
В этом примере
class A
{
int i = 0;
void F() {
int i = 1;
}
void G() {
i = 1;
}
}
в рамках метода F переменный экземпляр i скрыт локальной переменной i, но в рамках метода G экземпляр i все еще ссылается на переменный экземпляр.
Когда имя во внутренней области скрывает имя внешней области, скрываются все перегруженные вхождения данного имени. В этом примере
class Outer
{
static void F(int i) {}
static void F(string s) {}
class Inner
{
void G() {
F(1); // Invokes Outer.Inner.F
F("Hello"); // Error
}
static void F(long l) {}
}
}
вызов F(1) приводит к вызову F, объявленному в Inner, так как все внешние вхождения F скрыты внутренним объявлением. По той же причине вызов F("Hello") приводит к ошибке времени компиляции.
Скрытие через наследование
Скрытие имени через наследование происходит, когда классы или структуры повторно объявляют имена, унаследованные из базовых классов. Этот тип скрытия имени принимает одну из следующих форм.
· Константа, поле, свойство, событие или тип, представленные в классе или структуре, скрывают все члены базового класса с таким же именем.
· Метод, представленный в классе или структуре, скрывает все члены базового класса, не являющиеся методами, и все методы базового класса с такой же сигнатурой (имя метода и попадания, модификаторы и типы параметра).
· Индексатор, представленный в классе или структуре, скрывает все индексаторы базового класса с такой же сигнатурой (попадания и типы параметра).
Правила, управляющие объявлениями операторов (§10.10), делают невозможным для производного класса объявление оператора с сигнатурой, аналогичной сигнатуре оператора базового класса. Поэтому операторы никогда не скрывают друг друга.
В противоположность скрытию имени из внешней области, скрытие доступного имени из унаследованной области приводит к предупреждению. В этом примере
class Base
{
public void F() {}
}
class Derived: Base
{
public void F() {} // Warning, hiding an inherited name
}
объявление F в классе Derived приводит к выводу предупреждения. Скрытие наследуемого имени не является ошибкой, поскольку это препятствует отдельному развитию базовых классов. Например, вышеуказанная ситуация могла наступить, так как более поздняя версия Base представила метод F, который не был представлен в более ранней версии класса. Если бы вышеупомянутая ситуация была ошибочной, любое изменение, сделанное для базового класса в библиотеке класса отдельной версии, могло потенциально привести к недопустимости производных классов.
Предупреждение, вызванное скрытием унаследованного имени, можно устранить с помощью модификатора new:
class Base
{
public void F() {}
}
class Derived: Base
{
new public void F() {}
}
Модификатор new указывает, что метод F в классе Derived является "новым" и скрытие унаследованного члена выполняется намеренно.
Объявление нового члена приводит к скрытию унаследованного члена только в рамках области нового члена.
class Base
{
public static void F() {}
}
class Derived: Base
{
new private static void F() {} // Hides Base.F in Derived only
}
class MoreDerived: Derived
{
static void G() { F(); } // Invokes Base.F
}
В вышеуказанном примере объявление F в классе Derived приводит к скрытию метода F, унаследованного от Base, но, так как для нового метода F в классе Derived объявлен закрытый доступ, его область не распространяется на класс MoreDerived. Поэтому вызов F() в классе MoreDerived.G является допустимым и приводит к вызову Base.F.