Книга: C# 4.0: полное руководство

Управление доступом к членам класса

Управление доступом к членам класса

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

В языке С#, по существу, имеются два типа членов класса: открытые и закрытые, хотя в действительности дело обстоит немного сложнее. Доступ к открытому члену свободно осуществляется из кода, определенного за пределами класса. Именно этот тип члена класса использовался в рассматривавшихся до сих пор примерах программ. А закрытый член класса доступен только методам, определенным в самом классе. С помощью закрытых членов и организуется управление доступом.

Ограничение доступа к членам класса является основополагающим этапом объектно-ориентированного программирования, поскольку позволяет исключить неверное использование объекта. Разрешая доступ к закрытым

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

Модификаторы доступа

Управление доступом в языке C# организуется с помощью четырех модификаторов доступа:public, private, protected и internal. В этой главе основное внимание уделяется модификаторам доступа public и private. Модификатор protected применяется только в тех случаях, которые связаны с наследованием, и поэтому речь о нем пойдет в главе 11. А модификатор internal служит в основном для сборки, которая в широком смысле означает в C# разворачиваемую программу или библиотеку, и поэтому данный модификатор подробнее рассматривается в главе 16.

Когда член класса обозначается спецификатором public, он становится доступным из любого другого кода в программе, включая и методы, определенные в других классах. Когда же член класса обозначается спецификатором private, он может быть доступен только другим членам этого класса. Следовательно, методы из других классов не имеют доступа к закрытому члену (private) данного класса. Как пояснялось в главе 6, если ни один из спецификаторов доступа не указан, член класса считается закрытым для своего класса по умолчанию. Поэтому при создании закрытых членов класса спецификатор private указывать для них необязательно.

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

public string errMsg;
private double bal;
private bool isError(byte status) { // ...

Для того чтобы стали более понятными отличия между модификаторами public и private, рассмотрим следующий пример программы.

// Отличия между видами доступа public и private к членам класса.
using System;
class MyClass {
  private int alpha;    // закрытый    доступ, указываемый    явно
  int beta;    // закрытый доступ по умолчанию
  public int gamma; // открытый доступ
  // Методы, которым доступны члены alpha и beta данного класса.
  // Член класса может иметь доступ к закрытому члену этого же класса.
  public void SetAlpha(int а) {
    alpha = а;
  }
  public int GetAlpha() {
    return alpha;
  }
  public void SetBeta(int a) {
    beta = a;
  }
  public int GetBeta() {
    return beta;
  }
}
class AccessDemo {
  static void Main() {
    MyClass ob = new MyClass();
    // Доступ к членам alpha и beta данного класса
    // разрешен только посредством его методов,
    ob.SetAlpha(-99) ;
    ob.SetBeta(19) ;
    Console.WriteLine("ob.alpha равно " + ob.GetAlpha());
    Console.WriteLine("ob.beta равно " + ob.GetBeta());
    // Следующие виды доступа к членам alpha и beta
    // данного класса не разрешаются.
    // ob.alpha =10; // Ошибка! alpha - закрытый член!
    // ob.beta =9;    // Ошибка! beta - закрытый член!
    // Член gamma данного класса доступен непосредственно,
    // поскольку он является открытым, ob.gamma = 99;
  }
}

Как видите, в классе MyClass член alpha указан явно как private, член beta становится private по умолчанию, а член gamma указан как public. Таким образом, члены alpha и beta недоступны непосредственно из кода за пределами данного класса, поскольку они являются закрытыми. В частности, ими нельзя пользоваться непосредственно в классе AccessDemo. Они доступны только с помощью таких открытых (public) методов, как SetAlpha() и GetAlpha(). Так, если удалить символы комментария в начале следующей строки кода:

// ob.alpha =10; // Ошибка! alpha - закрытый член!

то приведенная выше программа не будет скомпилирована из-за нарушения правил доступа. Но несмотря на то, что член alpha недоступен непосредственно за пределами класса MyClass, свободный доступ к нему организуется с помощью методов, определенных в классе MyClass, как наглядно показывают методы SetAlpha() и GetAlpha().   Это же относится и к члену beta.

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

Организация закрытого и открытого доступа

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

•    Члены, используемые только в классе, должны быть закрытыми.

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

•    Если изменение члена приводит к последствиям, распространяющимся за пределы области действия самого члена, т.е. оказывает влияние на другие аспекты объекта, то этот член должен быть закрытым, а доступ к нему — контролируемым.

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

•    Методы, получающие и устанавливающие значения закрытых данных, должны быть открытыми.

•    Переменные экземпляра допускается делать открытыми лишь в том случае, если нет никаких оснований для того, чтобы они были закрытыми.

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

Практический пример организации управления доступом

Для чтобы стали понятнее особенности внутреннего механизма управления доступом, обратимся к конкретному примеру. Одним из самых характерных примеров объектно-ориентированного программирования служит класс, реализующий стек — структуру данных, воплощающую магазинный список, действующий по принципу "первым пришел — последним обслужен". Свое название он получил по аналогии со стопкой тарелок, стоящих на столе. Первая тарелка в стопке является в то же время последней использовавшейся тарелкой.

Стек служит классическим примером объектно-ориентированного программирования потому, что он сочетает в себе средства хранения информации с методами доступа к ней. Для реализации такого сочетания отлично подходит класс, в котором члены, обеспечивающие хранение информации в стеке, должны быть закрытыми, а методы доступа к ним — открытыми. Благодаря инкапсуляции базовых средств хранения информации соблюдается определенный порядок доступа к отдельным элементам стека из кода, в котором он используется.

Для стека определены две основные операции: поместить данные в стек и извлечь их оттуда. Первая операция помещает значение на вершину стека, а вторая — извлекает значение из вершины стека. Следовательно, операция извлечения является безвозвратной: как только значение извлекается из стека, оно удаляется и уже недоступно в стеке.

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

// Класс для хранения символов в стеке.
using System;
class Stack {
  // Эти члены класса являются закрытыми,
  char[] stck; // массив, содержащий стек
  int tos;    // индекс вершины стека
  // Построить пустой класс Stack для реализации стека заданного размера,
  public Stack(int size) {
    stck = new char[size]; // распределить память для стека
    tos = 0;
  }
  // Поместить символы в стек,
  public void Push(char ch) {
    if(tos==stck.Length) {
      Console.WriteLine(" - Стек заполнен.");
      return;
    }
    stck[tos] = ch;
    tos++;
  }
  // Извлечь символ из стека,
  public char Pop() {
    if(tos==0) {
      Console.WriteLine(" - Стек пуст.");
      return (char) 0;
    }
    tos-- ;
    return stck[tos];
  }
  // Возвратить значение true, если стек заполнен,
  public bool IsFull() {
    return tos==stck.Length;
  }
  // Возвратить значение true, если стек пуст,
  public bool IsEmpty() {
    return tos==0;
  }
  // Возвратить общую емкость стека,
  public int Capacity() {
    return stck.Length;
  }
  // Возвратить количество объектов, находящихся в данный момент в стеке,
  public int GetNum() {
    return tos;
  }
}

Рассмотрим класс Stack более подробно. В начале этого класса объявляются две следующие переменные экземпляра.

// Эти члены класса являются закрытыми,
char[] stck; // массив, содержащий стек
int tos;    // индекс    вершины стека

Массив stck предоставляет базовые средства для хранения данных в стеке (в данном случае — символов). Обратите внимание на то, что память для этого массива не распределяется. Это делается в конструкторе класса Stack. А член tos данного класса содержит индекс вершины стека.

Оба члена, tosnstck, являются закрытыми, и благодаря этому соблюдается принцип "последним пришел — первым обслужен". Если же разрешить открытый доступ к члену stck, то элементы стека окажутся доступными не по порядку. Кроме того, член tos содержит индекс вершины стека, где находится первый обслуживаемый в стеке элемент, и поэтому манипулирование членом tos в коде, находящемся за пределами класса Stack, следует исключить, чтобы не допустить разрушение самого стека. Но в то же время члены stckntos доступны пользователю класса Stack косвенным образом с помощью различных отрытых методов, описываемых ниже.

Рассмотрим далее конструктор класса Stack.

// Построить пустой класс Stack для реализации стека заданного размера,
public Stack(int size) {
  stck = new char[size]; // распределить память для стека
  tos = 0;
}

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

Открытый метод Push() помещает конкретный элемент в стек, как показано ниже.

// Поместить символы в стек,
public void Push(char ch) {
  if (tos==stck.Length) {
    Console.WriteLine(" - Стек заполнен.");
    return;
  }
  stck[tos] = ch;
  tos++;
}

Элемент, помещаемый в стек, передается данному методу в качестве параметра ch. Перед тем как поместить элемент в стек, выполняется проверка на наличие свободного места в базовом массиве, а именно: не превышает ли значение переменной tos длину массива stck. Если свободное место в массиве stck есть, то элемент сохраняется в нем по индексу, хранящемуся в переменной tos, после чего значение этой переменной инкрементируется. Таким образом, в переменной tos всегда хранится индекс следующего свободного элемента массива stck.

Для извлечения элемента из стека вызывается открытый метод Pop(), приведенный ниже.

// Извлечь символ из стека,
public char Рор() {
  if(tos==0) {
    Console.WriteLine (" - Стек пуст.");
    return (char) 0;
  }
  tos-- ;
  return stck[tos];
}

В этом методе сначала проверяется значение переменной tos. Если оно равно нулю, значит, стек пуст. В противном случае значение переменной tos декрементируется, и затем из стека возвращается элемент по указанному индексу.

Несмотря на то что для реализации стека достаточно методов Push() и Pop(), полезными могут оказаться и другие методы. Поэтому в классе Stack определены еще четыре метода: IsFull(), IsEmpty(), Capacity() и GetNum(). Эти методы предоставляют всю необходимую информацию о состоянии стека и приведены ниже.

// Возвратить значение true, если стек заполнен,
public bool IsFull() {
  return tos==stck.Length;
}
// Возвратить значение true, если стек пуст,
public bool IsEmpty() {
  return tos==0;
}
// Возвратить общую емкость стека,
public int Capacity() {
  return stck.Length;
}
// Возвратить количество объектов, находящихся в данный момент в стеке,
public int GetNum() {
  return tos;
}

Метод IsFull() возвращает логическое значение true, если стек заполнен, а иначе — логическое значение false. Метод IsEmpty() возвращает логическое значение true, если стек пуст, а иначе — логическое значение false. Для получения общей емкости стека (т.е. общего числа элементов, которые могут в нем храниться) достаточно вызвать метод Capacity(), а для получения количества элементов, хранящихся в настоящий момент в стеке, — метод GetNum(). Польза этих методов состоит в том, что для получения информации, которую они предоставляют, требуется доступ к закрытой переменной tos. Кроме того, они служат наглядными примерами организации безопасного доступа к закрытым членам класса с помощью открытых методов.

Конкретное применение класса Stack для реализации стека демонстрируется в приведенной ниже программе.

// Продемонстрировать применение класса Stack,
using System;
// Класс для хранения символов в стеке.
class Stack {
  // Эти члены класса являются закрытыми,
  char[] stck; // массив, содержащий стек
  int tos;    // индекс вершины стека
  // Построить пустой класс Stack для реализации стека заданного размера,
  public Stack(int size) {
    stck = new char[size]; // распределить память для стека
    tos = 0;
  }
  // Поместить символы в стек,
  public void Push(char ch) {
    if(tos==stck.Length) {
      Console.WriteLine(" - Стек заполнен.");
      return;
    }
    stck[tos] = ch;
    tos++;
  }
  // Извлечь символ из стека,
  public char Pop() {
    if(tos==0) {
      Console.WriteLine(" - Стек пуст.");
      return (char) 0;
    }
    tos-- ;
    return stck[tos];
  }
  // Возвратить значение true, если стек заполнен,
  public bool IsFull() {
    return tos==stck.Length;
  }
  // Возвратить значение true, если стек пуст,
  public bool IsEmpty() {
    return tos==0;
  }
  // Возвратить общую емкость стека,
  public int Capacity() {
    return stck.Length;
  }
  // Возвратить количество объектов, находящихся в данный момент в стеке,
  public int GetNum() {
    return tos;
  }
}
class StackDemo {
  static void Main() {
    Stack stk1 = new Stack(10);
    Stack stk2 = new Stack(10);
    Stack stk3 = new Stack(10);
    char ch;
    int i;
    // Поместить ряд символов в стек stk1.
    Console.WriteLine("Поместить символы А-J в стек stk1.");
    for(i=0; !stk1.IsFull(); i++)
      stk1.Push((char)('A' + i));
    if(stk1.IsFull())
      Console.WriteLine("Стек stk1 заполнен.");
    // Вывести содержимое стека stk1.
    Console.Write("Содержимое стека stk1: ");
    while( !stk1.IsEmpty()) {
      ch = stk1.Pop();
      Console.Write(ch);
    }
    Console.WriteLine();
    if(stk1.IsEmpty())
      Console.WriteLine("Стек stk1 пуст.");
    // Поместить дополнительные символы в стек stk1.
    Console.WriteLine("Вновь поместить символы А-J в стек stk1.");
    for(i=0; !stk1.IsFull(); i++)
      stk1.Push((char)('A' + i));
    // А теперь извлечь элементы из стека stk1 и поместить их в стек stk2.
    // В итоге элементы сохраняются в стеке stk2 в обратном порядке.
    Console.WriteLine("А теперь извлечь символы из стека stk1n" +
          "и поместить их в стек stk2.");
    while( !stk1.IsEmpty()) {
      ch = stk1.Pop();
      stk2.Push(ch);
    }
    Console.Write("Содержимое стека stk2: ");
    while( !stk2.IsEmpty() ) {
      ch = stk2.Pop();
      Console.Write(ch);
    }
    Console.WriteLine("n");
    // Поместить 5 символов в стек.
    Console.WriteLine("Поместить 5 символов в стек stk3.");
    for(i=0; i < 5; i++)
      stk3.Push((char) ('A' + i)) ;
    Console.WriteLine("Емкость стека stk3: " + stk3.Capacity());
    Console.WriteLine("Количество объектов в стеке stk3: " 
          + stk3.GetNum());
  }
}

При выполнении этой программы получается следующий результат.

Поместить символы А-J в стек stk1.
Стек stk1 заполнен.
Содержимое стека stk1: JIHGFEDCBA
Стек stk1 пуст.
Вновь поместить символы А-J в стек stk1.
А теперь извлечь символы из стека stk1
и поместить их в стек stk2.
Содержимое стека stk2: ABCDEFGHIJ
Поместить 5 символов в стек stk3.
Емкость стека stk3: 10
Количество объектов в стеке stk3: 5

Оглавление книги


Генерация: 1.099. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз