This lesson is not translated yet – the Russian original is below.
Класс описывает две вещи: что объект хранит и что он умеет. Первое – поля, второе – методы. Между ними стоят свойства: снаружи они выглядят как данные, а внутри работают как методы.
Поля – где живут данные
Вернёмся к чертежу машины. На чертеже написано, что у машины есть цвет и модель. Но сам чертёж не бывает красным: цвет появляется только у конкретной построенной машины.
Поле – это переменная, которая принадлежит объекту. Класс объявляет её один раз, а значение у каждого объекта своё:
- у красной Tesla поле
colorхранит"красный", у синей Toyota –"синий"; - изменение поля одного объекта не трогает остальные;
- поле живёт ровно столько, сколько живёт сам объект.
Поля почти всегда закрывают от внешнего кода. Причина простая: если пробег машины может поменять кто угодно и на что угодно, объект перестаёт отвечать за собственную корректность.
Зачем закрывать поля и где проходит граница – в уроке Инкапсуляция.
public class Car
{
// Поля объявлены в классе, но значения у каждого объекта свои
private string _color;
private int _mileage;
}
class Car {
private:
// Поля объявлены в классе, но значения у каждого объекта свои
std::string color_;
int mileage_ = 0;
};
type Car struct {
Color string // с большой буквы – видно снаружи пакета
mileage int // с маленькой – только внутри
}
class Car:
def __init__(self, color):
self._color = color # поле создаётся присваиванием в конструкторе
self._mileage = 0 # подчёркивание – договорённость «снаружи не трогать»
class Car {
readonly color: string;
#mileage = 0; // решётка закрывает поле на уровне языка
constructor(color: string) {
this.color = color;
}
}
public class Car {
// Поля закрыты: снаружи к ним не обратиться
private String color;
private int mileage;
}
class Car(val color: String) {
// Поле под свойством Kotlin создаёт сам, когда оно нужно
private var mileage: Int = 0
}
Методы – что объект умеет
На чертеже написано не только из чего машина состоит, но и что она делает: заводится, едет, тормозит. Едет при этом не чертёж – едет конкретная машина, и пробег накручивает себе она же.
Метод – это функция, которая живёт внутри класса. Отличие от обычной функции одно, но принципиальное: метод имеет доступ к полям своего объекта и обычно вызывается «у него».
Когда мы пишем myCar.Drive(120), мы не просим программу «проехать 120 километров» –
мы просим об этом конкретную машину. Она сама решит, что при этом изменится внутри.
Метод – хорошее место для проверок. Пробег нельзя увеличить на отрицательное число, и знает об этом сам объект, а не тот, кто его вызвал.
public void Drive(int km)
{
if (km <= 0) return; // проверка живёт внутри объекта
_mileage += km; // метод видит поля своего экземпляра
}
void Drive(int km) {
if (km <= 0) return; // проверка живёт внутри объекта
mileage_ += km; // метод видит поля своего экземпляра
}
// Получатель c – тот самый объект, «у которого» вызвали метод
func (c *Car) Drive(km int) {
if km <= 0 {
return
}
c.mileage += km
}
def drive(self, km): # self – объект, у которого вызвали метод
if km <= 0:
return
self._mileage += km
drive(km: number): void {
if (km <= 0) return; // проверка живёт внутри объекта
this.#mileage += km;
}
public void drive(int km) {
if (km <= 0) return; // проверка живёт внутри объекта
mileage += km;
}
fun drive(km: Int) {
if (km <= 0) return // проверка живёт внутри объекта
mileage += km
}
Свойства – поле снаружи, метод внутри
Как мы говорили выше, поля закрывают внутри объекта. Но данные всё равно кому-то нужны:
покупателю – цвет, сервису – пробег. Классический выход – методы-обёртки: GetColor для
цвета, GetMileage для пробега.
SetColor и SetMileage в этом списке нет, и это не забывчивость. Цвет машине задают на
заводе, пробег накручивает только поездка – значит, наружу отдано одно чтение: нет метода
– нет и способа изменить поле. Так объект решает не «показывать или прятать», а что
именно позволено делать с каждым значением: что-то отдаётся на чтение, что-то меняется
методом вроде Drive, а что-то не выходит наружу вовсе.
Обёрток при этом много: на каждое значение свой метод, а если менять всё-таки разрешено – то и второй. И обращение выглядит как вызов, хотя по смыслу это просто данные. Поэтому и придумали свойства.
Свойство выглядит в коде как поле, но за чтением и записью стоит метод. Значит, внутрь можно спрятать проверку, пересчёт или запрет на изменение – и внешний код об этом не узнает, потому что обращение к нему не меняется.
Механизм есть не везде: часть языков поддерживает свойства прямо в синтаксисе, а часть по-прежнему заставляет писать методы-обёртки руками. Как это устроено в выбранном языке:
public string Color { get; } // только чтение: задаётся в конструкторе
public int Mileage => _mileage; // только чтение: меняет метод Drive
// Свойств в C++ нет, их роль играют методы
const std::string& color() const { return color_; }
int mileage() const { return mileage_; }
// Сеттеров нет ни у того, ни у другого: снаружи оба значения только читают
// Свойств в Go нет. Открытое поле читают напрямую, а «только для чтения» –
// это закрытое поле и метод к нему
func (c *Car) Mileage() int {
return c.mileage
}
@property
def mileage(self): # обращение как к полю: car.mileage
return self._mileage
get mileage(): number { // обращение как к полю: car.mileage
return this.#mileage;
}
// Свойств в языке нет: по соглашению пишут геттеры и сеттеры
public String getColor() { return color; }
public int getMileage() { return mileage; }
// Сеттеров нет ни у того, ни у другого: снаружи оба значения только читают
val color: String = "белый" // val – читать можно, менять нельзя
var mileage: Int = 0
private set // менять может только сам класс
В C# свойства встроены в язык – это { get; set; } в объявлении.В C++ свойств нет: наружу значения отдают обычные методы.В Go свойств нет вовсе: есть поля и методы, а видимостью управляет регистр первой буквы имени.В Python это декоратор @property над обычным методом.В TypeScript это аксессоры get и set.В Java свойств нет: их роль по соглашению играют геттеры и сеттеры.В Kotlin свойство – основной способ объявить данные, а поле появляется само, если оно нужно.
Поле, свойство, метод: в чём разница
| Поле | Свойство Разница смысловая, а не техническая: свойство – это метод, и внутрь можно положить хоть запрос в базу. Технически ничто не мешает, но car.mileage выглядит как чтение данных, и долгой работы за ним никто не ждёт – примерно как выключатель на стене, который вместо света вызывает лифт. |
Метод | |
|---|---|---|---|
| Что это | переменная объекта | доступ к данным через код | действие объекта |
| Как выглядит вызов | car.color |
car.color |
car.drive(120) |
| Можно ли добавить проверку | нет | да | да |
| Что обычно означает | состояние | состояние | поведение |
Ещё раз, на всякий случай: таблица про смысл, а не про синтаксис – набор механизмов сильно зависит от языка. Свойств нет в C++, Go и Java – там их роль играют методы. А в Си нет и методов: есть структуры с данными и отдельные функции, которые эти структуры принимают аргументом.
Практическое правило: если обращение выглядит как получение данных, это свойство; если
как поступок – это метод. Поэтому car.mileage – свойство, а car.drive(120) – метод,
хотя оба в итоге работают с одним и тем же полем.
Пример: класс Car целиком
Здесь есть все три: закрытое поле с пробегом, свойство только для чтения и метод, который меняет пробег и следит за корректностью.
public class Car
{
// Поле – переменная внутри объекта, снаружи она не видна
private int _mileage;
public Car(string color, string model)
{
Color = color;
Model = model;
}
// Свойства: выглядят как поля, но за ними стоят методы
public string Color { get; }
public string Model { get; }
// Только для чтения: изменить пробег извне нельзя
public int Mileage => _mileage;
// Метод – действие, которое объект умеет выполнять
public void Drive(int km)
{
if (km <= 0)
{
Console.WriteLine("Расстояние должно быть больше нуля");
return;
}
_mileage += km;
Console.WriteLine($"{Model} проехал {km} км, всего {_mileage} км");
}
}
class Car {
public:
Car(std::string color, std::string model)
: color_(std::move(color)), model_(std::move(model)) {}
// Свойств в C++ нет: доступ дают методы
const std::string& color() const { return color_; }
const std::string& model() const { return model_; }
// Только для чтения: сеттера просто нет
int mileage() const { return mileage_; }
// Метод – действие, которое объект умеет выполнять
void Drive(int km) {
if (km <= 0) {
std::cout << "Расстояние должно быть больше нуля" << std::endl;
return;
}
mileage_ += km;
std::cout << model_ << " проехал " << km
<< " км, всего " << mileage_ << " км" << std::endl;
}
private:
// Поля – переменные внутри объекта, снаружи не видны
std::string color_;
std::string model_;
int mileage_ = 0;
};
// Свойств в Go нет: есть поля и методы. Видимостью управляет регистр первой буквы –
// mileage с маленькой буквы не виден за пределами пакета.
type Car struct {
Color string
Model string
mileage int
}
func NewCar(color, model string) *Car {
return &Car{Color: color, Model: model}
}
// Роль «свойства только для чтения» играет обычный метод
func (c *Car) Mileage() int {
return c.mileage
}
func (c *Car) Drive(km int) {
if km <= 0 {
fmt.Println("Расстояние должно быть больше нуля")
return
}
c.mileage += km
fmt.Printf("%s проехал %d км, всего %d км\n", c.Model, km, c.mileage)
}
class Car:
def __init__(self, color, model):
self._color = color
self._model = model
# Подчёркивание – договорённость: «снаружи не трогать»
self._mileage = 0
# Свойство: снаружи выглядит как поле, внутри это метод
@property
def color(self):
return self._color
@property
def model(self):
return self._model
@property
def mileage(self):
return self._mileage
# Метод – действие, которое объект умеет выполнять
def drive(self, km):
if km <= 0:
print("Расстояние должно быть больше нуля")
return
self._mileage += km
print(f"{self._model} проехал {km} км, всего {self._mileage} км")
class Car {
// Поле с решёткой закрыто на уровне языка, а не по договорённости
#mileage = 0;
constructor(
public readonly color: string,
public readonly model: string,
) {}
// Свойство: обращаются как к полю, а работает метод
get mileage(): number {
return this.#mileage;
}
drive(km: number): void {
if (km <= 0) {
console.log("Расстояние должно быть больше нуля");
return;
}
this.#mileage += km;
console.log(`${this.model} проехал ${km} км, всего ${this.#mileage} км`);
}
}
public class Car {
private final String color;
private final String model;
// Поле – данные объекта, наружу закрыто
private int mileage;
public Car(String color, String model) {
this.color = color;
this.model = model;
}
// Свойств в языке нет: доступ дают геттеры
public String getColor() {
return color;
}
public String getModel() {
return model;
}
public int getMileage() {
return mileage;
}
public void drive(int km) {
if (km <= 0) {
System.out.println("Расстояние должно быть больше нуля");
return;
}
mileage += km;
System.out.println(model + " проехал " + km + " км, всего " + mileage + " км");
}
}
class Car(val color: String, val model: String) {
// Свойство с закрытым сеттером: читать могут все, менять – только сам класс
var mileage: Int = 0
private set
fun drive(km: Int) {
if (km <= 0) {
println("Расстояние должно быть больше нуля")
return
}
mileage += km
println("$model проехал $km км, всего $mileage км")
}
}
Почему пробег закрыт
Попробуйте мысленно открыть поле mileage наружу. Ничего не сломается сразу – но теперь
любой код в программе может выставить пробег в ноль или в минус тысячу. И когда это
случится, искать виноватого придётся по всему проекту.
Пока пробег меняет только метод drive, у ошибки одно место. Это и есть смысл границы
между «внутри объекта» и «снаружи»: не спрятать данные, а сузить круг подозреваемых.
Итог: поля – это память объекта, методы – его поведение, свойства – аккуратная дверь к памяти. Разница между свойством и методом не техническая, а смысловая: свойство отвечает на вопрос «какой объект сейчас», метод – «что он умеет сделать».