С классы свойства и противопоказания
Общая структура типа C# представляет собой описание типа и определение полей и методов. Поля – хранят данные, методы ими манипулируют (изменяют значения).
Свойства созданы как комбинация метода и поля в одном члене типа для лаконичности и безопасности кода.
Лаконичность достигается тем, что для считывания или изменения значения поля в свойствах используются два метода. Безопасность – ограничением доступа к полям типа только свойствами. Для программиста типа, свойство выглядит методом, поскольку его нужно определять и реализовывать. Для программиста использующего объект типа – полем, поскольку обращение к полю и свойству идентичны. Инкапсуляция.
Для доступа к значению поля в свойстве используются методы, которые создаются средой выполнения, но реализуются программистом. Их два – get и set. Get используется в случае чтения значения поля и называется акссесором, set – при изменении значения, называется мутатором. Общее название для мутатора и аксессора – методы доступа.
Объявление свойства
Методы определяются внутри блока типа. Сигнатура в последовательности при объявлении:
уровень доступа;
тип свойства;
имя свойства.
После сигнатуры определения следует два блока кода с реализацией аксессора и/или мутатора:
public class GrandMa
{
private int fullAge = 69; / закрытое поле для хранения возраста. Доступ к полю возможен только из свойства и методов класса.
public int FullAge
{
get
{
return fullAge;
}
set
{
if (value > 0)
{
fullAge = value;
}
}
}
}
Допустимо реализовывать свойство только с одним из методов. Свойство, содержащее только аксессор называется полем только для чтения, свойство с одним мутатором – свойством только для записи.
Аксессор get
Если в свойстве использован акссессор, то подобно методу, свойство должно возвращать значение поля, которое акссессор считывает. Тип поля должен соответствовать типу возвращаемого значения свойства.
Блок кода аксессора похож на реализацию метода и выполняется по всем правилам синтаксиса С#. Пример объявления свойства только с аксессором:
class Elephant
{
private string elName; // Закрытое поле. Считывание значения возможно только методами и свойствами класса.
public string ElName // открытое свойство для считывания значения поля elName. Возвращает значение типа string.
{
// реализация аксессора
get
{
return elName; // возврат значения поля elName
}
}
}
Использование аксессора в пользовательском в коде эквивалентно считыванию на прямую из поля:
Elephant el1 = new Elephant();// создание объекта типа Elephant
System.Console.WriteLine(el1.ElName); // вывод значения поля elName на системную консоль
В блоке реализации аксессора можно менять значение считываемого поля, но крайне не рекомендуется, поскольку это может иметь массу побочных эффектов, которые при увеличении объёма кода трудно отследить:
class Elephant
{
private string elName; // Закрытое поле. Считывание значения возможно только методами и свойствами класса.
public string ElName // открытое свойство для считывания значения поля elName. Возвращает значение типа string.
{
// реализация аксессора
get
{
return elName + “ Сидорович”; // возврат значения поля elName с изменением. Изменения будут происходить при каждом использовании свойства внутри объекта. Плохой стиль, для изменения значения лучше использовать мутатор.
}
}
}
Мутатор set
Мутатор, или метод set по спецификации вызова поход на метод с возвращаемым значение типа void и одним параметром. public void SetValue (object value). Среда выполнения сама его вызывает в точке указанной программистом с прописанной им реализацией.
При реализации мутатора в блоке кода свойства нужно помнить о неявном участии параметра value, который выполняет функцию буфера, промежуточной переменной, значение которой нужно предать полю типа.
Пример реализации:
class Elephant
{
private string elName; // Закрытое поле. Считывание значения возможно только методами и свойствами класса.
public string ElName // открытое свойство для считывания значения поля elName. Возвращает значение типа string.
{
get
{
return elName;
}
// Реализация мутатора
set
{
elName = value; // полю elName присваивается значение неявного параметра value.
}
}
}
В блоке кода мутатора нельзя вводить локальную переменную value, поскольку уже есть такая переменная, неявно переданная в виде параметра. Такой код компилятор не пропустит и выдаст ошибку:
class Elephant
{
private string elName; // Закрытое поле. Считывание значения возможно только методами и свойствами класса.
public string ElName // открытое свойство для считывания значения поля elName. Возвращает значение типа string.
{
// Реализация мутатора
set
{
String value = “Фыва”; // нельзя, параметр value уже определен.
elName = value; // полю elName присваивается значение неявного параметра value.
}
}
}
Метод доступа set используется неявно при присвоении значения свойству, как в следующем примере:
Elephant el1 = new Elephant();// создание объекта типа Elephant
El1.ElName = “Никки”; // присваиваем значение полю elName через свойство ElName неявно вызывается мутатор.
System.Console.WriteLine(el1.ElName); // вывод значения поля elName на системную консоль
Модификаторы свойств и уровни доступа
Свойства, подобно типам в которых определяются по уровню доступа могут быть: открытыми (public), частными (private), защищенными (protected). Доступ к методам свойства осуществляется в соответствии с модификатором.
Свойство может быть статичным (static), и будет доступным без необходимости создавать экземпляр типа, в котором оно определено.
Допустимо объявлять свойства виртуальными, для возможности их переопределения в производных классах с помощью ключевого слова override или переопределения и запечатывания при помощи ключевого слова sealed.
Свойство можно сделать абстрактным при помощи ключевого слова abstract, оставив блок реализации пустым, в дальнейшем реализовав свойство в дочерних (производных) классах.
Применительно к определению свойств запрещено одновременное употребление следующих пар модификаторов:
static – virtual;
static – abstract;
static – override.
Допустимо использовать разные уровни доступа для аксессора и мутатора, что бывает удобно, если нужно обеспечить доступность чтения поля, но при этом защитить его от записи методами производных классов.
Пример:
private string description = “Какое-то описание”;
// создаем свойство для чтения/записи значения в поле description с разными модификаторами доступа для аксессора и мутатора
public string Description
{
// открытый аксессор
get
{
return description;
}
// защищенный мутатор
protected set
{
description = value;
}
}
Если для одного из методов доступа не указан модификатор доступа, то компилятор вставит его неявно руководствуясь модификатором доступа свойства.
Автоматически реализуемые свойства
Программист при реализации свойства связывает его с полем. Часто поля и свойства имеют одинаковые имена, чтобы программист, взглянув, мог сразу догадаться, для работы с каким полем создано свойство.
При работе с типами, в которых реализованы свойства, поля вовсе не вызываются, поскольку любой доступ к ним организован через свойства. Зачем в таком случае создавать поля вручную? И если код свойств не требует изысков программирования, то не проще ли доверить их создание компилятору.
Автоматически реализуемые свойства – это свойства, которые автоматически реализует компилятор, он же создает и поле, для доступа к которому создается свойство.
Сигнатура определения автоматически реализуемого свойства:
тип имя { get; set; }
Сам блок инструкций в таком свойстве отсутствует и для мутатора и для аксессора. После ключевых слов get и set ставится символ завершения инструкции «;».
Автоматически реализуемые свойства доступны в версии C# 3.0 и новее. С 6 версии спецификации языка доступна возможность объявить и инициализировать скрытое поле через упрощенный синтаксис свойства, вот так:
public string Nickname { get; set; } = “Забияка”;
Источник
Весь реальный мир состоит из объектов. Города состоят из районов, в каждом районе есть свои названия улиц, на каждой улице находятся жилые дома, которые также состоят из объектов.
Практически любой материальный предмет можно представить в виде совокупности объектов, из которых он состоит. Допустим, что нам нужно написать программу для учета успеваемости студентов. Можно представить группу студентов, как класс языка C++. Назовем его Students.
class Students {
// Имя студента
std::string name;
// Фамилия
std::string last_name;
// Пять промежуточных оценок студента
int scores[5];
// Итоговая оценка за семестр
float average_ball;
};
Основные понятия
Классы в программировании состоят из свойств и методов. Свойства — это любые данные, которыми можно характеризовать объект класса. В нашем случае, объектом класса является студент, а его свойствами — имя, фамилия, оценки и средний балл.
У каждого студента есть имя — name и фамилия last_name . Также, у него есть промежуточные оценки за весь семестр. Эти оценки мы будем записывать в целочисленный массив из пяти элементов. После того, как все пять оценок будут проставлены, определим средний балл успеваемости студента за весь семестр — свойство average_ball.
Методы — это функции, которые могут выполнять какие-либо действия над данными (свойствами) класса. Добавим в наш класс функцию calculate_average_ball(), которая будет определять средний балл успеваемости ученика.
- Методы класса — это его функции.
- Свойства класса — его переменные.
class Students {
public:
// Функция, считающая средний балл
void calculate_average_ball()
{
int sum = 0; // Сумма всех оценок
for (int i = 0; i < 5; ++i) {
sum += scores[i];
}
// считаем среднее арифметическое
average_ball = sum / 5.0;
}
// Имя студента
std::string name;
// Фамилия
std::string last_name;
// Пять промежуточных оценок студента
int scores[5];
private:
// Итоговая оценка за семестр
float average_ball;
};
Функция calculate_average_ball() просто делит сумму всех промежуточных оценок на их количество.
Модификаторы доступа public и private
Все свойства и методы классов имеют права доступа. По умолчанию, все содержимое класса является доступным для чтения и записи только для него самого. Для того, чтобы разрешить доступ к данным класса извне, используют модификатор доступа public. Все функции и переменные, которые находятся после модификатора public, становятся доступными из всех частей программы.
Закрытые данные класса размещаются после модификатора доступа private. Если отсутствует модификатор public, то все функции и переменные, по умолчанию являются закрытыми (как в первом примере).
Обычно, приватными делают все свойства класса, а публичными — его методы. Все действия с закрытыми свойствами класса реализуются через его методы. Рассмотрим следующий код.
class Students {
public:
// Установка среднего балла
void set_average_ball(float ball)
{
average_ball = ball;
}
// Получение среднего балла
float get_average_ball()
{
return average_ball;
}
std::string name;
std::string last_name;
int scores[5];
private:
float average_ball;
};
Мы не можем напрямую обращаться к закрытым данными класса. Работать с этими данными можно только посредством методов этого класса. В примере выше, мы используем функцию get_average_ball() для получения средней оценки студента, и set_average_ball() для выставления этой оценки.
Функция set_average_ball() принимает средний балл в качестве параметра и присваивает его значение закрытой переменной average_ball. Функция get_average_ball() просто возвращает значение этой переменной.
Программа учета успеваемости студентов
Создадим программу, которая будет заниматься учетом успеваемости студентов в группе. Создайте заголовочный файл students.h, в котором будет находиться класс Students.
/* students.h */
#include <string>
class Students {
public:
// Установка имени студента
void set_name(std::string student_name)
{
name = student_name;
}
// Получение имени студента
std::string get_name()
{
return name;
}
// Установка фамилии студента
void set_last_name(std::string student_last_name)
{
last_name = student_last_name;
}
// Получение фамилии студента
std::string get_last_name()
{
return last_name;
}
// Установка промежуточных оценок
void set_scores(int student_scores[])
{
for (int i = 0; i < 5; ++i) {
scores[i] = student_scores[i];
}
}
// Установка среднего балла
void set_average_ball(float ball)
{
average_ball = ball;
}
// Получение среднего балла
float get_average_ball()
{
return average_ball;
}
private:
// Промежуточные оценки
int scores[5];
// Средний балл
float average_ball;
// Имя
std::string name;
// Фамилия
std::string last_name;
};
Мы добавили в наш класс новые методы, а также сделали приватными все его свойства. Функция set_name() сохраняет имя студента в переменной name, а get_name() возвращает значение этой переменной. Принцип работы функций set_last_name() и get_last_name() аналогичен.
Функция set_scores() принимает массив с промежуточными оценками и сохраняет их в приватную переменную int scores[5].
Теперь создайте файл main.cpp со следующим содержимым.
/* main.cpp */
#include <iostream>
#include “students.h”
int main()
{
// Создание объекта класса Student
Students student;
std::string name;
std::string last_name;
// Ввод имени с клавиатуры
std::cout << “Name: “;
getline(std::cin, name);
// Ввод фамилии
std::cout << “Last name: “;
getline(std::cin, last_name);
// Сохранение имени и фамилии в объект класса Students
student.set_name(name);
student.set_last_name(last_name);
// Оценки
int scores[5];
// Сумма всех оценок
int sum = 0;
// Ввод промежуточных оценок
for (int i = 0; i < 5; ++i) {
std::cout << “Score ” << i+1 << “: “;
std::cin >> scores[i];
// суммирование
sum += scores[i];
}
// Сохраняем промежуточные оценки в объект класса Student
student.set_scores(scores);
// Считаем средний балл
float average_ball = sum / 5.0;
// Сохраняем средний балл в объект класса Students
student.set_average_ball(average_ball);
// Выводим данные по студенту
std::cout << “Average ball for ” << student.get_name() << ” ”
<< student.get_last_name() << ” is ”
<< student.get_average_ball() << std::endl;
return 0;
}
В самом начале программы создается объект класса Students. Дело в том, что сам класс является только описанием его объекта. Класс Students является описанием любого из студентов, у которого есть имя, фамилия и возможность получения оценок.
Объект класса Students характеризует конкретного студента. Если мы захотим выставить оценки всем ученикам в группе, то будем создавать новый объект для каждого из них. Использование классов очень хорошо подходит для описания объектов реального мира.
После создания объекта student, мы вводим с клавиатуры фамилию, имя и промежуточные оценки для конкретного ученика. Пускай это будет Вася Пупкин, у которого есть пять оценок за семестр — две тройки, две четверки и одна пятерка.
Введенные данные мы передаем set-функциям, которые присваивают их закрытым переменным класса. После того, как были введены промежуточные оценки, мы высчитываем средний балл на основе этих оценок, а затем сохраняем это значение в закрытом свойстве average_ball, с помощью функции set_average_ball().
Скомпилируйте и запустите программу.
Отделение данных от логики
Вынесем реализацию всех методов класса в отдельный файл students.cpp.
/* students.cpp */
#include <string>
#include “students.h”
// Установка имени студента
void Students::set_name(std::string student_name)
{
Students::name = student_name;
}
// Получение имени студента
std::string Students::get_name()
{
return Students::name;
}
// Установка фамилии студента
void Students::set_last_name(std::string student_last_name)
{
Students::last_name = student_last_name;
}
// Получение фамилии студента
std::string Students::get_last_name()
{
return Students::last_name;
}
// Установка промежуточных оценок
void Students::set_scores(int scores[])
{
for (int i = 0; i < 5; ++i) {
Students::scores[i] = scores[i];
}
}
// Установка среднего балла
void Students::set_average_ball(float ball)
{
Students::average_ball = ball;
}
// Получение среднего балла
float Students::get_average_ball()
{
return Students::average_ball;
}
А в заголовочном файле students.h оставим только прототипы этих методов.
/* students.h */
#pragma once /* Защита от двойного подключения заголовочного файла */
#include <string>
class Students {
public:
// Установка имени студента
void set_name(std::string);
// Получение имени студента
std::string get_name();
// Установка фамилии студента
void set_last_name(std::string);
// Получение фамилии студента
std::string get_last_name();
// Установка промежуточных оценок
void set_scores(int []);
// Установка среднего балла
void set_average_ball(float);
// Получение среднего балла
float get_average_ball();
private:
// Промежуточные оценки
int scores[5];
// Средний балл
float average_ball;
// Имя
std::string name;
// Фамилия
std::string last_name;
};
Такой подход называется абстракцией данных — одного из фундаментальных принципов объектно-ориентированного программирования. К примеру, если кто-то другой захочет использовать наш класс в своем коде, ему не обязательно знать, как именно высчитывается средний балл. Он просто будет использовать функцию calculate_average_ball() из второго примера, не вникая в алгоритм ее работы.
Над крупными проектами обычно работает несколько программистов. Каждый из них занимается написанием определенной части продукта. В таких масштабах кода, одному человеку практически нереально запомнить, как работает каждая из внутренних функций проекта. В нашей программе, мы используем оператор потокового вывода cout, не задумываясь о том, как он реализован на низком уровне. Кроме того, отделение данных от логики является хорошим тоном программирования.
В начале обучения мы говорили о пространствах имен (namespaces). Каждый класс в C++ использует свое пространство имен. Это сделано для того, чтобы избежать конфликтов при именовании переменных и функций. В файле students.cpp мы используем оператор принадлежности :: перед именем каждой функции. Это делается для того, чтобы указать компилятору, что эти функции принадлежат классу Students.
Создание объекта через указатель
При создании объекта, лучше не копировать память для него, а выделять ее в в куче с помощью указателя. И освобождать ее после того, как мы закончили работу с объектом. Реализуем это в нашей программе, немного изменив содержимое файла main.cpp.
/* main.cpp */
#include <iostream>
#include “students.h”
int main()
{
// Выделение памяти для объекта Students
Students *student = new Students;
std::string name;
std::string last_name;
// Ввод имени с клавиатуры
std::cout << “Name: “;
getline(std::cin, name);
// Ввод фамилии
std::cout << “Last name: “;
getline(std::cin, last_name);
// Сохранение имени и фамилии в объект класса Students
student->set_name(name);
student->set_last_name(last_name);
// Оценки
int scores[5];
// Сумма всех оценок
int sum = 0;
// Ввод промежуточных оценок
for (int i = 0; i < 5; ++i) {
std::cout << “Score ” << i+1 << “: “;
std::cin >> scores[i];
// суммирование
sum += scores[i];
}
// Сохраняем промежуточные оценки в объект класса Student
student->set_scores(scores);
// Считаем средний балл
float average_ball = sum / 5.0;
// Сохраняем средний балл в объект класса Students
student->set_average_ball(average_ball);
// Выводим данные по студенту
std::cout << “Average ball for ” << student->get_name() << ” ”
<< student->get_last_name() << ” is ”
<< student->get_average_ball() << std::endl;
// Удаление объекта student из памяти
delete student;
return 0;
}
При создании статического объекта, для доступа к его методам и свойствам, используют операция прямого обращения — «.» (символ точки). Если же память для объекта выделяется посредством указателя, то для доступа к его методам и свойствам используется оператор косвенного обращения — «->».
Конструктор и деструктор класса
Конструктор класса — это специальная функция, которая автоматически вызывается сразу после создания объекта этого класса. Он не имеет типа возвращаемого значения и должен называться также, как класс, в котором он находится. По умолчанию, заполним двойками массив с промежуточными оценками студента.
class Students {
public:
// Конструктор класса Students
Students(int default_score)
{
for (int i = 0; i < 5; ++i) {
scores[i] = default_score;
}
}
private:
int scores[5];
};
int main()
{
// Передаем двойку в конструктор
Students *student = new Students(2);
return 0;
}
Мы можем исправить двойки, если ученик будет хорошо себя вести, и вовремя сдавать домашние задания. А на «нет» и суда нет 🙂
Деструктор класса вызывается при уничтожении объекта. Имя деструктора аналогично имени конструктора, только в начале ставится знак тильды ~. Деструктор не имеет входных параметров.
#include <iostream>
class Students {
public:
// Деструктор
~Students()
{
std::cout << “Memory has been cleaned. Good bye.” << std::endl;
}
};
int main()
{
Students *student = new Students;
// Уничтожение объекта
delete student;
return 0;
}
Следующий урок: Конструкторы и деструкторы классов в C++ →.
Источник