Swift | Вопросы собесов
2.23K subscribers
30 photos
962 links
Download Telegram
🤔 Какой constraint на тип ключа накладывает Swift dictionary?

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


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Расскажи про кодирование и декодирование в user defaults

UserDefaults — это хранилище для сохранения простых данных (строки, числа, массивы). Но если нужно сохранить сложные объекты, их сначала кодируют в `Data` (Codable), а затем сохраняют.

🟠Сохранение и загрузка простых данных
Для примитивных типов (строки, числа, массивы, словари) UserDefaults работает без кодирования:
let defaults = UserDefaults.standard

// Сохранение
defaults.set("Иван", forKey: "username")
defaults.set(25, forKey: "age")

// Чтение
let name = defaults.string(forKey: "username") ?? "Нет имени"
let age = defaults.integer(forKey: "age")

print(name, age) // Иван 25


🟠Сохранение сложных объектов (Codable)
Если нужно сохранить свой объект, сначала его нужно закодировать в Data.
struct User: Codable {
let name: String
let age: Int
}


Сохранение в UserDefaults
let user = User(name: "Иван", age: 25)
let defaults = UserDefaults.standard

if let encoded = try? JSONEncoder().encode(user) {
defaults.set(encoded, forKey: "user")
}


Загрузка из UserDefaults
if let savedData = defaults.data(forKey: "user"),
let savedUser = try? JSONDecoder().decode(User.self, from: savedData) {
print(savedUser.name, savedUser.age) // Иван 25
}


🟠Удаление данных из `UserDefaults`
Чтобы удалить ключ:
defaults.removeObject(forKey: "user")


🟠Сохранение массива объектов
Можно сохранить массив объектов, просто закодировав его:
let users = [
User(name: "Иван", age: 25),
User(name: "Анна", age: 30)
]

if let encoded = try? JSONEncoder().encode(users) {
defaults.set(encoded, forKey: "users")
}

// Читаем массив обратно
if let savedData = defaults.data(forKey: "users"),
let savedUsers = try? JSONDecoder().decode([User].self, from: savedData) {
print(savedUsers) // [{name: Иван, age: 25}, {name: Анна, age: 30}]
}


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
🤔 Как реализовать composition full layout?

В SwiftUI или Jetpack Compose composition full layout означает создание интерфейса из переиспользуемых, независимых компонентов. Он строится через композицию небольших вью или функций, каждая из которых отвечает за отдельную часть интерфейса, что повышает читаемость и тестируемость.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Расскажи про структуры

Структуры (struct) – это один из основных типов данных в Swift. Они значимые (value type), то есть при передаче копируются, а не передаются по ссылке.

🚩Главные особенности `struct`

Value type – при передаче копируется (в отличие от классов)
Нет ARC (автоматического подсчета ссылок) – производительность выше
Могут иметь методы, свойства, инициализаторы
Работают с протоколами (protocol-oriented programming)
Не поддерживают наследование

🚩Пример структуры

struct Car {
var model: String
var year: Int

func description() -> String {
return "\(model) – \(year)"
}
}

let car1 = Car(model: "Tesla", year: 2023)
var car2 = car1 // Создается копия

print(car1.description()) // Tesla – 2023
print(car2.description()) // Tesla – 2023

car2.year = 2024 // Изменяем car2, но car1 остается прежним
print(car1.year) // 2023
print(car2.year) // 2024


🚩Мутируемые структуры (`mutating`)

По умолчанию методы не могут изменять свойства структуры. Чтобы изменить их внутри метода, нужно использовать mutating
struct Counter {
var count = 0

mutating func increment() {
count += 1
}
}

var counter = Counter()
counter.increment()
print(counter.count) // 1


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Каким образом подключая интерфейс мы получаем поведение?

Протокол (интерфейс) описывает поведение, а при подключении к нему класс или структура обязываются реализовать методы, тем самым приобретая определённое поведение. Это форма абстракции и инверсии управления.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Чем отличаются слабые и сильные ссылки?

В Swift ссылки (references) на объекты могут быть сильными (strong) и слабыми (weak). Они отличаются способом управления памятью и временем жизни объектов, на которые ссылаются.

🚩Сильные ссылки (Strong References)

Сильная ссылка (strong reference) удерживает объект в памяти. Пока существует хотя бы одна сильная ссылка на объект, он не будет удалён из памяти.
Сильные ссылки используются по умолчанию и обеспечивают, что объект остаётся доступным до тех пор, пока он необходим.
class Person {
var name: String
init(name: String) {
self.name = name
}
}

var person1: Person? = Person(name: "Alice")
var person2: Person? = person1 // person2 имеет сильную ссылку на тот же объект, что и person1

person1 = nil // Объект все еще удерживается в памяти благодаря person2
print(person2?.name) // Output: Alice

person2 = nil // Теперь объект будет удалён из памяти, так как нет сильных ссылок


🚩Слабые ссылки (Weak References)

Слабая ссылка (weak reference) не удерживает объект в памяти. Если объект больше не имеет сильных ссылок, он будет удалён из памяти, даже если на него существуют слабые ссылки.
Слабые ссылки используются для предотвращения циклических ссылок, которые могут привести к утечкам памяти.
class Person {
var name: String
init(name: String) {
self.name = name
}
}

class Apartment {
var tenant: Person?
init(tenant: Person?) {
self.tenant = tenant
}
}

var alice: Person? = Person(name: "Alice")
var apartment: Apartment? = Apartment(tenant: alice)

// Создание слабой ссылки для предотвращения циклической зависимости
class Tenant {
var name: String
weak var apartment: Apartment? // Слабая ссылка
init(name: String) {
self.name = name
}
}

let tenant = Tenant(name: "Bob")
apartment?.tenant = alice

alice = nil // Объект Person будет удалён из памяти, так как больше нет сильных ссылок
print(apartment?.tenant?.name) // Output: nil


🚩Основные различия между сильными и слабыми ссылками

🟠Удержание в памяти
Сильные ссылки: Удерживают объект в памяти. Объект будет освобождён только тогда, когда все сильные ссылки на него будут удалены.
Слабые ссылки: Не удерживают объект в памяти. Объект будет освобождён, когда не останется сильных ссылок.

🟠Использование
Сильные ссылки: Используются по умолчанию для обеспечения сохранности объектов в памяти.
Слабые ссылки: Используются для предотвращения циклических ссылок и утечек памяти, особенно в структурах данных с взаимосвязанными объектами.

🟠Обнуление
Сильные ссылки: Не обнуляются автоматически при удалении объекта из памяти.
Слабые ссылки: Автоматически обнуляются, когда объект, на который они ссылаются, удаляется из памяти.

Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2💊1
🤔 Что нужно, чтобы нарисовать вьюшку кастомной формы?

Чтобы создать вью с нестандартной формой, нужно:
1. Переопределить метод draw(_:) в кастомном UIView.
2. Использовать API Core Graphics, например UIBezierPath, CGContext.
3. Для маски или формы — можно задать layer.mask или использовать CAShapeLayer.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Что такое runLoop?

RunLoop — это фундаментальный механизм в iOS и macOS, который управляет циклом обработки событий в приложении. Он отслеживает и обрабатывает входящие события, такие как нажатия клавиш, касания экрана, таймеры и сетевые запросы, и поддерживает приложение в активном состоянии, пока оно не завершится.

🚩Основные аспекты `RunLoop`

🟠Цикл обработки событий
RunLoop постоянно выполняет цикл, ожидая входящие события и обрабатывая их по мере поступления. Этот цикл состоит из нескольких этапов: ожидание события, обработка события и повтор цикла.

🟠Режимы (Modes)
RunLoop может работать в разных режимах, которые определяют, какие источники событий будут отслеживаться и обрабатываться. Основные режимы включают default и tracking (для событий отслеживания, таких как прокрутка). В каждой итерации RunLoop обрабатывает события только для текущего режима.
RunLoop.current.run(mode: .default, before: Date.distantFuture)     


🟠Источники событий (Event Sources)
RunLoop может отслеживать различные источники событий, такие как таймеры (Timer), порты (Port), ввод пользователей (такие как касания экрана и клики мыши), а также пользовательские источники (Input Source).

🟠Таймеры
RunLoop может управлять таймерами, которые выполняют задачи через определенные интервалы времени.
     let timer = Timer(timeInterval: 1.0, repeats: true) { _ in
print("Timer fired!")
}
RunLoop.current.add(timer, forMode: .default)


🟠Обработка событий
RunLoop используется для обработки событий в основном потоке (main thread) приложения. Это особенно важно для поддержания отзывчивости пользовательского интерфейса, поскольку все взаимодействия с UI происходят в основном потоке.

🚩Пример использования `RunLoop`

import Foundation

class Example {
var timer: Timer?

func startRunLoop() {
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true)
RunLoop.current.run()
}

@objc func timerFired() {
print("Timer fired!")
}
}

let example = Example()
example.startRunLoop()


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
🤔 Какие есть варианты обезопасить работу с историей изображений?

- Использовать NSCache с автоматическим удалением;
- Хранить изображения по UUID или hash;
- Проверять размер файла, тип данных;
- Очищать кэш при нехватке памяти (didReceiveMemoryWarning);
- Удалять устаревшие записи по дате.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥1
🤔 Какие проблемы можно получить , если оставить контекст?

Если оставить контекст в замыкании, не принимая во внимание возможные проблемы, это может привести к нескольким серьезным проблемам, особенно в многопоточном и асинхронном программировании.

🚩Проблемы

🟠Утечки памяти (Retain Cycles)
Одной из самых распространенных проблем является утечка памяти из-за циклов удержания (retain cycles). Это происходит, когда два или более объекта удерживают ссылки друг на друга, препятствуя освобождению памяти. В этом примере closure захватывает self, что создает цикл удержания: MyClass держит сильную ссылку на closure, а closure держит сильную ссылку на self.
class MyClass {
var value: Int = 0
var closure: (() -> Void)?

func setupClosure() {
closure = {
self.value += 1
}
}
}

let instance = MyClass()
instance.setupClosure()


🟠Непредсказуемое поведение и условия гонки (Race Conditions)
Когда замыкания захватывают изменяемый контекст, это может привести к условиям гонки и непредсказуемому поведению, особенно при работе в многопоточном окружении. Если метод increment вызывается из разных потоков, это может привести к условиям гонки и некорректному изменению значения count.
class Counter {
var count = 0

func increment() {
DispatchQueue.global().async {
self.count += 1
}
}
}

let counter = Counter()
counter.increment()


🟠Задержки в освобождении ресурсов
Если замыкания захватывают тяжелые ресурсы (например, файлы, сети), это может привести к задержкам в их освобождении, что может негативно сказаться на производительности приложения. Если FileHandler освобождается, но замыкание все еще захватывает file, это может привести к задержке в освобождении файлового дескриптора.
class FileHandler {
var file: File?

func processFile() {
DispatchQueue.global().async {
self.file?.read()
}
}
}


🟠Потеря захваченных данных
Когда используется слабая ссылка (weak), замыкание может обнаружить, что захваченный объект освобожден, что приводит к тому, что слабая ссылка становится nil. Это требует дополнительных проверок и обработки.
class MyClass {
var value: Int = 0
var closure: (() -> Void)?

func setupClosure() {
closure = { [weak self] in
guard let strongSelf = self else { return }
strongSelf.value += 1
}
}
}


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Где фреймы — наиболее яркий пример использования?

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


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Почему Apple предпочитает использовать value type по умолчанию?

Apple предпочитает value types (структуры struct) по умолчанию в Swift по нескольким причинам

🟠Безопасность многопоточного кода
struct копируется при передаче, а не передается по ссылке, как class.
Это снижает вероятность гонки данных (data race), когда один поток изменяет объект, а другой читает его одновременно.
В многопоточной среде это делает код более безопасным.
struct Point {
var x: Int
var y: Int
}

var p1 = Point(x: 1, y: 2)
var p2 = p1 // p2 - это копия, изменения в p2 не затронут p1

p2.x = 10

print(p1.x) // 1
print(p2.x) // 10


🟠Производительность
struct хранятся в стеке, а не в куче, что делает их создание и удаление быстрее.
Куча (heap) требует управления памятью (ARC – Automatic Reference Counting), а struct — нет.
class MyClass { var value = 0 }  // В куче (heap), управляется ARC
struct MyStruct { var value = 0 } // В стеке (stack), копируется при передаче


🟠Предсказуемость и неизменяемость
struct ведут себя как примитивные типы (Int, Double), что делает код предсказуемым.
Их изменение происходит локально, без неожиданных эффектов в других частях программы.
class Car {
var speed: Int
init(speed: Int) { self.speed = speed }
}

var car1 = Car(speed: 60)
var car2 = car1 // car2 - это ссылка на тот же объект

car2.speed = 100 // Изменение затрагивает car1!

print(car1.speed) // 100 (хотя мы меняли car2!)


🟠Использование в стандартной библиотеке
Swift изначально построен на struct:
Int, Double, Bool, Array, Dictionary, String — это структуры.
Это делает язык более безопасным и производительным.
var arr1 = [1, 2, 3]
var arr2 = arr1 // Копия массива, а не ссылка!

arr2.append(4)

print(arr1) // [1, 2, 3] (не изменился!)
print(arr2) // [1, 2, 3, 4] (новый массив)


🚩Когда использовать `class`?

Хотя struct — предпочтительный выбор, class нужен, когда:
Нужна ссылочная семантика (например, объект должен изменяться в разных местах кода).
Есть сложные иерархии наследования.
Требуется работа с Objective-C (NSObject).

Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
🤔 Какие есть ссылки в ARC?

Сильные (strong), слабые (weak), и безвладельческие (unowned). Сильные удерживают объект, остальные — нет, и нужны для предотвращения retain cycle.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
💊3
🤔 Что может быть ключом и значением для dictionary?

Словарь (Dictionary) представляет собой коллекцию пар ключ-значение, где каждый ключ должен быть уникальным. Чтобы использовать какой-либо тип в качестве ключа словаря, этот тип должен соответствовать протоколу Hashable. Это требование обусловлено тем, что Swift использует хеш-таблицу для хранения элементов словаря, что обеспечивает быстрый доступ к его элементам.

🚩Ключи

Должны быть уникальными: Каждый ключ в словаре должен быть уникальным. При попытке добавить в словарь элемент с ключом, который уже существует в словаре, старое значение будет заменено на новое.
Должны соответствовать протоколу Hashable: Это означает, что тип ключа должен иметь способность быть правильно хешированным. Большинство базовых типов Swift (например, String, Int, Double и др.) уже соответствуют Hashable, поэтому их можно использовать в качестве ключей без дополнительных усилий.

🚩Значения

Могут быть любого типа: Значения в словаре могут быть любого типа, и они не обязаны соответствовать протоколу Hashable.
Могут повторяться: Разные ключи могут иметь одинаковые значения.
var personAge: [String: Int] = ["John": 30, "Sara": 25]


🚩Собственные типы в качестве ключей

Вы также можете использовать собственные пользовательские типы в качестве ключей словаря, но для этого ваш тип должен соответствовать протоколу Hashable. Это включает в себя реализацию требуемых методов для сравнения на равенство (==) и хеширования (hash(into:)).
struct Person: Hashable {
var name: String
var id: Int
}

var peopleDictionary: [Person: String] = [Person(name: "John", id: 1): "Engineer"]


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
🤔 Какие есть способы вёрстки?

- Auto Layout (наиболее гибкий и универсальный).
- Stack Views (удобные группы элементов с автоматической раскладкой).
- Frame-based layout (устаревший способ вручную).
- Constraint anchors и визуальный язык разметки (VFL).
- Interface Builder (через Xcode UI).
- SwiftUI (декларативный подход).


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Какие есть инструменты для работы с потоками?

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

🟠GCD (Grand Central Dispatch) – главный инструмент для потоков
GCD – это низкоуровневая технология, позволяющая управлять задачами (тасками) в очередях (DispatchQueue).
DispatchQueue.global(qos: .background).async {
print("Фоновый поток")

DispatchQueue.main.async {
print("Вернулись в главный поток")
}
}


🟠OperationQueue – более удобный API для задач
OperationQueue – это более гибкая и объектно-ориентированная альтернатива GCD.
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2 // Ограничение на 2 задачи одновременно

queue.addOperation {
print("Операция 1")
}

queue.addOperation {
print("Операция 2")
}


🟠Actors – безопасная работа с потоками в Swift 5.5+
С actor можно работать с потоками без гонок данных, потому что все его свойства защищены от одновременного доступа.
actor Counter {
private var value = 0

func increment() {
value += 1
}

func getValue() -> Int {
return value
}
}

let counter = Counter()

Task {
await counter.increment()
print(await counter.getValue()) // Потокобезопасный доступ
}


🟠Task & Async/Await (Swift 5.5+) – современный подход к асинхронности
С async/await код становится читаемым и удобным.
func fetchData() async -> String {
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 секунда задержки
return "Данные загружены"
}

Task {
let result = await fetchData()
print(result)
}


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1🔥1
🤔 Как обращаются к объектам класса и объектам структуры?

Классы передаются по ссылке, а структуры по значению.
- К объектам класса обращаются через ссылку (let obj = MyClass()), изменения в одной переменной отразятся на всех экземплярах.
- К объектам структуры обращаются как к копиям, каждое присваивание создает новый объект (let obj = MyStruct()).


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Какие есть фрейм ворк механизмы в айос для выполненной бэкраунд задач?

В iOS для выполнения фоновых задач существуют несколько ключевых механизмов:

🚩Основные механизмы

🟠Grand Central Dispatch (GCD): Используется для асинхронного выполнения задач на глобальных или пользовательских очередях.
DispatchQueue.global(qos: .background).async {
// Фоновая задача
}


🟠OperationQueue: Высокоуровневый API для управления очередями операций с возможностью указания зависимостей.
let queue = OperationQueue()
queue.addOperation {
// Фоновая операция
}


🟠Background Fetch: Позволяет приложению периодически загружать новые данные в фоновом режиме.
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// Фоновое обновление данных
completionHandler(.newData)
}


🟠BGTaskScheduler: Новый фреймворк для планирования и выполнения фоновых задач.
import BackgroundTasks

func scheduleBackgroundTask() {
let request = BGAppRefreshTaskRequest(identifier: "com.example.app.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
}


🟠URLSession Background Transfers: Выполнение загрузки и выгрузки данных в фоновом режиме.
let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
let session = URLSession(configuration: configuration)
let url = URL(string: "https://example.com/largefile")!
let task = session.downloadTask(with: url)
task.resume()


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Какие есть состояния в жизненном цикле приложения?

– Not running — приложение не запущено.
– Inactive — запущено, но не получает событий (например, переход между экранами).
– Active — активно и реагирует на действия пользователя.
– Background — работает в фоновом режиме (например, обновляет контент).
– Suspended — приложение в памяти, но не выполняет код (возможен переход в этот режим из background).


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
🤔 Что под капотом стэка?

Это абстрактная структура данных, работающая по принципу LIFO (Last In, First Out), что означает "последний пришёл — первый вышел". Это значит, что последний добавленный элемент будет первым при извлечении из стека. Под капотом реализации стека могут быть разные, и они зависят от конкретного языка программирования и задач, которые необходимо решить.

🟠Массивы
Один из самых распространённых способов реализации стека — это использование массива. В такой реализации элементы стека хранятся в массиве, и индекс последнего элемента (вершина стека) отслеживается отдельной переменной.
struct Stack<Element> {
private var storage: [Element] = []

mutating func push(_ element: Element) {
storage.append(element)
}

mutating func pop() -> Element? {
return storage.popLast()
}

func peek() -> Element? {
return storage.last
}

var isEmpty: Bool {
return storage.isEmpty
}
}


🟠Связные списки
Стек можно реализовать с использованием связных списков, где каждый элемент списка содержит данные и ссылку на следующий элемент в стеке. Вершина стека в такой реализации — это начало связного списка.
class Node<Element> {
var value: Element
var next: Node?

init(value: Element) {
self.value = value
}
}

struct Stack<Element> {
private var head: Node<Element>?

mutating func push(_ element: Element) {
let node = Node(value: element)
node.next = head
head = node
}

mutating func pop() -> Element? {
let node = head
head = head?.next
return node?.value
}

func peek() -> Element? {
return head?.value
}

var isEmpty: Bool {
return head == nil
}
}


🟠Стек вызовов
Это системный стек, который используется во время выполнения программы для хранения информации о вызовах функций/методов. Он хранит адреса возврата, параметры функций, локальные переменные и другие данные, необходимые для управления вызовами функций и их возврата.

🚩Зачем он нужен?

Обратную польскую нотацию для вычисления арифметических выражений. Управление вызовами функций в программном стеке. Поддержка операций undo в приложениях.

Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
🤔 Что известно про IRS?

IRS может означать разное в зависимости от контекста, но в iOS-разработке, если ты имел в виду:
- Input Response System (в неформальной терминологии) — реакция на ввод (клавиатура, тап, жест).
- В архитектуре Swift часто это относится к реакции на события пользовательского ввода.
Если ты имел в виду другое значение (например, из налогообложения или архитектурных систем) — уточни, пожалуйста.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
💊7🤔2