double arrow

Уровень доступа protected и internal


Язык С# накладывает строгое ограничение на прямой доступ к закрытым элементам так, что даже производный класс, тесно связанный с базовым классом, не может получить доступа к закрытым (private) членам базового класса.

Методы производных классов, как правило, могут обращаться к членам базового класса, которые были объявлены с уровнем доступа public, protected и internal путем непосредственного использования их имен.

Использование уровня доступа protected (защищенный) предлагает промежуточный уровень защиты между уровнями доступа public и private. Доступ к protected членам базового класса может быть осуществлен только в этом базовом классе или в любых классах, производных от этого базового класса.

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

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

using System;

//Определение класса точки неявно наследует все из класса Object

public class Point2

{

// Данные:

protected int x, y; // Координаты точки

// Методы:

public Point2() // конструктор по умолчанию (без аргументов)

{

// здесь происходит неявное обращение к конструктору Object

x=100; y=100; // если явно не указать значения x и y, то они будут равны 0

}

public Point2(int a, int b) // конструктор

{

// здесь происходит неявное обращение к конструктору Object

x=a; y=b;

}

// Открытые функции доступа к закрытым данным

public void SetX(int a){ x=a; }

public int GetX(){ return x; }

public void SetY(int a){ y=a; }

public int GetY(){ return y; }

// Функции общего назначения

public void Move(int dx, int dy) { x+=dx; y+=dy; }

}

class Circle4: Point2

{

// Данные:

private double radius; // радиус окружности

// Методы:

public Circle4()// конструктор по умолчанию (без аргументов)

{

// здесь происходит неявное обращение к конструктору Object

// если явно не указать значения x и у, то они будут равны 0

x=100; y=100; // Ошибки уже нет!

radius=10;

}

public Circle4(int a, int b, double r) // конструктор

{

// здесь имеет место неявное обращение к конструктору класса Point (x=y=0)

x=a; y=b; // Ошибки уже нет!

radius=r;

}

public int GetRad(){ return radius; }

public void SetRad(double r){ if(r>0) radius=r; }

public double Area(){ return Math.Pi*Math.Pow(radius,2); }

}

Использование защищенных переменных в базовом классе создает две проблемы. В-первых, объекту производного класса уже можно будет не использовать открытую функцию доступа к закрытой переменной базового класса. Следовательно, объект производного класса мог бы легко присвоить недопустимое значение унаследованным защищенным данным. Второй проблемой использования защищенных данных является то, что методы производного класса, вероятнее всего, будут написаны в зависимости от реализации базового класса. На практике же, производные классы должны зависеть только от возможностей, предоставляемых базовым классом (т.е. от методов, не принадлежащих приватному уровню доступа), а не от реализации членов этого базового класса. И напротив, объявление переменных базового класса приватными позволяет программистам изменять реализацию базового класса без необходимости изменения реализации производного класса. Поэтому по возможности следует избегать включения защищенных данных в базовый класс. Вместо этого лучше включить не приватные методы, имеющие доступ к приватным данным, что позволит поддерживать согласованное состояние объектов. Примером такой реализации может служить ранее описанный нами класс Circle3.

Теперь рассмотрим еще один вариант построения класса окружности.

class Circle5: Point

{

// Данные:

private double radius; // радиус окружности

// Методы:

public Circle2()// конструктор по умолчанию (без аргументов)

{

// здесь происходит неявное обращение к конструктору Object

// в результате которого значения x и у станут равными 0

SetX(100); SetY(100); radius=10;

}

// конструктор с явным обращением к конструктору базового класса Point

public Circle2(int a, int b, double r) : base(x, y)

{

radius=r;

}

public double Area(){ return Math.Pi*Math.Pow(radius, 2); }

}

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

В свою очередь от класса окружности можно породить и другие классы, например, класс цилиндра.

class Cylinder: Circle5

{

// Данные:

private double height; // высота цилиндра

// Методы:

public Cylinder()// конструктор по умолчанию (без аргументов)

{

// здесь происходит неявное обращение к конструктору Circle

height=25;

}

// конструктор с явным обращением к конструктору базового класса Circle

public Cylinder(int a, int b, double r, double h) : base(x, y, r)

{

height=h;

}

public double GetVolume(){ return Area() * height; }

}

Ссылка на объект базового класса

Это универсальная ссылка для любого объекта производного типа, наследующего данный базовый класс.

using System;

namespace Inheritance_3

{

class X

{

public int q0 = 0;

public int q = 0;

public void fun0(){ Console.WriteLine("class X, fun0()"); }

public void fun1(){ Console.WriteLine("class X, fun1()"); }

}

class Y:X

{

new public int q = 0;

new public void fun1(){ Console.WriteLine("class Y, fun1()"); }

public void fun00(){ Console.WriteLine("class Y, fun00()"); }

}

class Z:X

{

new public int q = 0;

new public void fun1(){ Console.WriteLine("class Z, fun1()"); }

public void fun00(){ Console.WriteLine("class Z, fun00()"); }

}

class StartClass

{

static void Main(string[] args)

{

X x = null; // Просто ссылка!

// Объекты–представители производных классов–наследников.

Y y = new Y(); y.fun0(); y.fun00(); y.fun1();

Z z = new Z(); z.fun0(); z.fun00(); z.fun1();

// Настройка базовой ссылки.

x = y; x.fun0(); x.fun1(); x.q = 100; x.q0 = 125;

x = z; x.fun0(); x.fun1(); x.q = 100; x.q0 = 125;

}

}

}

Результат:

class X, fun0()

class Y, fun00()

class Y, fun1()

class X, fun0()

class Z, fun00()

class Z, fun1()

class X, fun0()

class X, fun1()

class X, fun0()

class X, fun1()

Вопросов не будет, если рассмотреть структуру экземпляра класса Y, то можно увидеть в нем следующее содержимое:

В частности, по ссылке на объект класса X, настроенного на экземпляр производного класса, можно увидеть, что все элементы класса Х вошли в состав класса Y. Естественно, что туда же вошли и все элементы, объявленные в классе Y. Содержимое экземпляра класса Z и соответствующий "вид" от ссылки x выглядят аналогичным образом.

Протоклассы

В С# классы разрабатываются для достижения определённых целей. Чаще всего программист начинает с нечётко очерченной идеи, которая постепенно, по мере созревания проекта, пополняется деталями. Иногда дело заканчивается двумя классами, весьма похожими друг на друга. Чтобы избежать дублирования кодов в этих классах, следует их разбить на две части, поместив общую часть в родительский класс, а отличающиеся части оставить в производных. Классы, созданные только для того, чтобы концентрировать общую часть кода, называются протоклассами (seed classes). Протоклассы не всегда абстрактны, но очень часто сами по себе бесполезны. Рассмотрим следующий листинг:

class Circle

{

int x, y;

Color col;

int rad;

public Circle ( int a, int b, int r, Color c)

{ x=10; y=10; rad=r; col=c; }

public void SetPoint(a, b){ x=a; y=b; }

public void SetColor(Color c){ col=c; }

public void Display(Graphics g){…}

public void SetRadius(int a){ if(a>0 rad=a; }

}

class Rect

{

int x, y;

Color col;

int x2, y2;

public:

Rect( int b, int a, int a2, int b2, Color c );

public void Set(a, b){ x=a; y=b; }

public void SetSecondPoint(a,b){ x=a; y=b; }

public void SetColor(Color c){ col=c; }

public void Display(Graphics g){…}

}

Здесь представлены два класса: Circle и Rect, которые имеют и общие свойства, и разные. Конструктор класса Circle имеет четыре аргумента, а конструктор класса Rect - пять. Ясно, что общая часть этих классов должна быть извлечена и помещена в родительский класс. Рассмотрим возможное решение.

// создание общего родительского класса

class ProtoClass

{

int x, y;

Color col;

public ProtoClass (int a, int b, Color c);

public void SetPoint(a, b){ x=a; y=b; }

public void SetColor(Color c){ col=c; }

public void Display(Graphics g){…}

}

Теперь при создании классов окружности и прямоугольника в качестве родительского класса можно использовать созданный нами протокласс:

class Circle2: ProtoClass

{

int rad;

// передача аргументов конструктору базового класса

public Circle ( int a, int b, int r, Color c): base(a, b, c){ rad=r; }

public void SetRadius(int a){ if(a>0 rad=a; }

}

class Rect2: public ProtoClass

{

int x2, y2;

// передача аргументов конструктору базового класса

public Rect( int b, int a, int a2, int b2, Color c ): base(a, b, c){ x2=a2; y2=b2; };

public void SetSecondPoint(a, b){ x=a; y=b; }

}

void main()

{

// использование двух производных классов

Circle2 c(100, 100, 30, Color.Red);

Rect2 r(10, 10, 60, 60, Color.Blue);

}


В нашем случае, иерархическая структура дерева наследования будет выглядеть следующим образом.

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


Сейчас читают про: