Към съдържанието
wannacode

Капсулация

Проблем

Минус 500 €

В миналия урок сложихме проверката за теглене в метод. Само че полето е отворено и всеки може да пише в него направо:

Account account = new Account("Мария", 100);
account.Withdraw(500);
account.Balance = -500;
Console.WriteLine(account.Balance);
Изход
-500

Методът правилно отказа да изтегли 500 €. Следващият ред просто записа баланса на ръка. Проверката е там, но може да се заобиколи. Трябва начин да кажем „балансът се пипа само от сметката“.

Идеята

private: само класът пипа

Сложи пред полето private вместо public:

class Account
{
private string owner;
private double balance;
// ...
}

Сега до полето стига само кодът вътре в класа Account. Отвън редът

account.balance = -500;

вече не се компилира:

Грешка при компилиране
error CS0122: 'Account.balance' is inaccessible due to its protection level

private и public се казват модификатори за достъп. Те казват кой може да вижда поле, метод или конструктор. Класът крие данните си и ги показва само през свои методи. Това се казва капсулация.

В C# скритите полета се пишат с малка буква: balance, owner. Затова в конструктора пишем this.balance = balance;: полето и параметърът вече се казват еднакво.

По-точно, за любопитните
  • Без модификатор полето или методът в C# са private. Затова до този урок пишехме public навсякъде.
  • Клас без модификатор е internal: видим в целия проект (сборката), но не и от други проекти. Затова class Product работи навсякъде в нашата програма.
  • Има и protected: видимо в класа и в наследниците му. Ще го срещнем в модул „Наследяване и полиморфизъм“.

Идеята

Да четеш, без да пипаш

Отвън вече не можем и да прочетем баланса. Затова класът дава метод, който само го връща:

Program.cs
class Account
{
private string owner;
private double balance;
public Account(string owner, double balance)
{
this.owner = owner;
this.balance = balance;
}
public string Owner
{
get { return owner; }
}
public double Balance
{
get { return balance; }
}
public void Deposit(double amount)
{
if (amount > 0)
{
balance += amount;
}
}
public bool Withdraw(double amount)
{
if (amount > 0 && amount <= balance)
{
balance -= amount;
return true;
}
return false;
}
}
class Program
{
static void Main()
{
Account account = new Account("Мария", 100);
account.Withdraw(30);
account.Deposit(50);
Console.WriteLine(account.Owner + ": " + account.Balance + " €");
}
}
Изход
Мария: 120 €

Balance е свойство (property). Отвън се ползва като поле: account.Balance. Вътре е метод: get връща стойността на скритото поле balance. Това е getter-ът на C#. Сменя се балансът само през Deposit и Withdraw, а те проверяват.

Withdraw вече връща bool: дали тегленето е минало. Така който тегли, разбира дали е успял.

Account account = new Account("Мария", 100);
account.Withdraw(500);
account.Deposit(50);
Console.WriteLine(account.Balance);
Стъпка по стъпка, с думи
  1. balance е private: стъклото значи, че отвън до полето няма път.
  2. Тегленето минава през проверката в метода: 500 е повече от 100. Отказано, балансът не се пипа.
  3. Внасянето също се проверява: 50 е положителна сума. Методът я добавя.
  4. Свойството Balance дава навън копие на баланса. Самото поле остава скрито.

Пробвай

Отказано

Какво ще отпечата програмата?

Account account = new Account("Мария", 100);
Console.WriteLine(account.Withdraw(150));
account.Deposit(-20);
Console.WriteLine(account.Balance);
Отговор

Идеята

Setter: промяна с проверка

Понякога полето трябва да може да се сменя отвън, но не с каквато и да е стойност. Възрастта на животно може да се промени, но не може да е отрицателна:

class Animal
{
private int age;
public int Age
{
get { return age; }
set
{
if (value >= 0)
{
age = value;
}
}
}
}
Animal cat = new Animal();
cat.Age = 4;
cat.Age = -3;
Console.WriteLine(cat.Age);
Изход
4

Свойството вече има и set: това е setter-ът. При cat.Age = -3 се изпълнява кодът в set, а value е стойността отдясно на =, тук -3. Проверката не минава и възрастта остава 4.

Идеята

Кое поле да има setter?

Не всяко поле има нужда от setter, а някои нямат нужда и от getter. Помисли какво трябва да може да се прави с продукта отвън:

  • името и цената се задават при създаването и после само се четат: само getter;
  • бройката в количката се сменя, но поне 1: getter и setter с проверка.
class Product
{
private string name;
private double price;
private int quantity;
public Product(string name, double price)
{
this.name = name;
this.price = price;
this.quantity = 1;
}
public string Name
{
get { return name; }
}
public double Price
{
get { return price; }
}
public int Quantity
{
get { return quantity; }
set
{
if (value >= 1)
{
quantity = value;
}
}
}
}
Product coffee = new Product("Кафе", 7.50);
coffee.Quantity = 3;
coffee.Quantity = 0;
Console.WriteLine(coffee.Name + " x" + coffee.Quantity);
Изход
Кафе x3

Правилото е: започни с private за всичко и отваряй само каквото трябва. Setter, който записва всичко без проверка, връща проблема от първата карта.

Идеята

Свойства накратко

Свойство, което само чете и записва поле без проверка, може да се напише на един ред. Скритото поле C# прави сам:

class Product
{
public string Name { get; set; }
public double Price { get; set; }
public int Quantity { get; set; } = 1;
}
Product coffee = new Product { Name = "Кафе", Price = 7.50 };
Console.WriteLine(coffee.Name + " x" + coffee.Quantity + ": " + coffee.Price + " €");
Изход
Кафе x1: 7.5 €

{ get; set; } е автоматично свойство. С = 1 то има стойност по подразбиране. new Product { Name = "Кафе", Price = 7.50 } е инициализатор на обект: създава обекта и задава свойствата му в едни скоби.

Можеш да скриеш само записа. { get; private set; } се чете отвсякъде, а се променя само в класа. { get; } се задава само в конструктора:

class Account
{
public string Owner { get; }
public double Balance { get; private set; }
public Account(string owner, double balance)
{
Owner = owner;
Balance = balance;
}
public void Deposit(double amount)
{
if (amount > 0)
{
Balance += amount;
}
}
}
Account account = new Account("Мария", 100);
account.Deposit(20);
Console.WriteLine(account.Owner + ": " + account.Balance + " €");
Изход
Мария: 120 €

Ред account.Balance = -500; в Main не се компилира:

Грешка при компилиране
error CS0272: The property or indexer 'Account.Balance' cannot be used in this context because the set accessor is inaccessible

Пробвай

Кое липсва?

Балансът трябва да се вижда само в класа. Какво пише на празното място?
class Account
{
     double balance;
}

Пробвай

Намери грешката

Полето е private, а балансът пак става -500:

Account account = new Account();
account.Balance = 100;
account.Withdraw(30);
account.Balance = -500;
Console.WriteLine(account.Balance);
Изход
-500

Кой ред в класа пробива защитата?

Избери реда с грешката.

Упражнение

Скрити данни

Реши задачите в редактора си. Всички полета са private.

1. Човек. Person с име и възраст, конструктор, свойства Name и Age само за четене и Introduce().

Изход
Мария е на 34 г.
Здравей, аз съм Мария и съм на 34 г.

2. Сметка без минус. Account със собственик и баланс. Внася се само положителна сума. Тегленето връща дали е минало и не пуска баланса под 0. Балансът се чете, но не се записва отвън.

Main
Account account = new Account("Петър", 50);
account.Deposit(25);
account.Deposit(-10);
Console.WriteLine(account.Withdraw(100));
Console.WriteLine(account.Withdraw(40));
Console.WriteLine("Баланс: " + account.Balance + " €");
Изход
False
True
Баланс: 35 €

3. Животно. Animal с вид и възраст. Видът се задава в конструктора и само се чете. Възрастта има set, който отказва отрицателно число и печата съобщение. Конструкторът също минава през тази проверка.

Main
Animal cat = new Animal("котка", 3);
cat.Age = 4;
cat.Age = -1;
Console.WriteLine(cat.Species + ", " + cat.Age + " г.");
Изход
Невалидна възраст: -1
котка, 4 г.

Още задачи

  • Оценка. Клас Grade с име на предмет и оценка от 2 до 6. Setter-ът отказва всичко извън тези граници. Пробвай с 5, 7 и 1.
  • Продукт с отстъпка. Към Product от урока добави метод за отстъпка в проценти, който приема само от 1 до 90%. Цената няма setter.

Тест

Провери се

1. Полето balance е private. Откъде може да се променя?
2. Защо сметката няма setter за баланса?
3. В set на свойство какво е value?
4. Какво значи капсулация?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·