JavaRush /Курси /Swift SELF /Шляхи до файлів: URL vs String

Шляхи до файлів: URL vs String

Swift SELF
Рівень 55 , Лекція 0
Відкрита

1. Тема шляхів важливіша, ніж здається

Коли ви тільки починаєте програмувати, шлях до файла виглядає як звичайний текст: "/tmp/library.json". На перший погляд, логічно зберігати його в String, друкувати, склеювати, відокремлювати розширення… і спокійно жити. У побуті ми так само записуємо адреси: «вул. Шевченка, 10, кв. 5» — просто рядок. Що може піти не так?

Піти може багато чого. Щойно у вас зʼявляється CLI-застосунок, а не «одне завдання в Web-IDE», ви раптом стикаєтеся з тим, що шлях потрібно складати з частин, робити відносним або абсолютним, коректно працювати з пробілами, розширеннями, каталогами та друкувати в логах так, щоб не вводити вас в оману.

Тож запамʼятаймо правило дня:

Всередині застосунку шляхи ми зберігаємо як URL (file URL), а не як String.
Рядок зʼявляється лише на межі — коли потрібно показати дані користувачеві або прийняти ввід.

2. Що не так зі шляхом як із рядком

Якщо ви зберігаєте шлях як рядок, дуже легко непомітно почати вручну робити те, що за вас уже вміють стандартна бібліотека та Foundation. І саме тут виникають «тихі баги» — найнеприємніші, бо застосунок не завершується аварійно, а просто поводиться дивно.

Уявімо типовий «рядковий шлях»:

let base = "/tmp"
let file = base + "/LibraryCLI" + "/" + "library" + ".json"
print(file) // /tmp/LibraryCLI/library.json

На вигляд усе добре, але тепер додамо реальність:

  • десь ви забудете слеш, і вийде "/tmpLibraryCLI";
  • десь додасте зайвий слеш, і вийде "/tmp//LibraryCLI";
  • десь імʼя матиме пробіли, і частина коду почне «лікувати» пробіли костилями;
  • десь знадобиться розширення, і ви почнете перевіряти, чи є крапка.

У підсумку у вас буде багато коду, присвяченого рядкам, а не завданню застосунку.

З URL ми позбуваємося цієї самодіяльності: замість «склеювання» ми додаємо компоненти шляху, замість ручного додавання .json — додаємо розширення, а замість здогадок можемо запитати в URL, який у нього останній компонент.

3. URL у Swift — це не лише про інтернет

Слово URL у багатьох асоціюється з вебом: https://example.com. І це нормально — більшість людей уперше бачить URL саме там. Але в Swift URL — це універсальний тип адреси ресурсу, а файлова система теж є ресурсом.

Сьогодні ми використовуємо URL строго як файловий URL — тобто як шлях у файловій системі. Це означає, що в таких URL:

  • схема зазвичай file://...
  • isFileURL == true
  • можна отримати шлях для файлової системи через url.path

Найважливіше: для файлового URL майже ніколи не слід починати з URL(string:).
URL(string:) призначений для URL-рядків на кшталт https://..., і він намагається інтерпретувати рядок за правилами URL, з екрануванням тощо. Файловий шлях — інша історія.

Правильне створення файлового URL

Зараз ми закріпимо базовий прийом, який ви будете використовувати постійно. Якщо у вас є шлях у вигляді рядка (наприклад, із конфігурації або з введення користувача), перетворюйте його на файловий URL так:


import Foundation

let file = URL(fileURLWithPath: "/tmp/library.json")
print(file.isFileURL) // true
print(file.path)      // /tmp/library.json

Якщо ви точно знаєте, що це каталог, корисно прямо сказати про це:

import Foundation

let dir = URL(fileURLWithPath: "/tmp/LibraryCLI", isDirectory: true)
print(dir.path)           // /tmp/LibraryCLI
print(dir.absoluteString) // file:///tmp/LibraryCLI/

Зверніть увагу на маленьку деталь: в absoluteString каталог часто закінчується слешем. Це не «кривий вивід» — це нормальне представлення URL.

4. path і absoluteString: два різні представлення

Тут легко заплутатися, тож розкладімо все акуратно. У URL є багато рядкових представлень, але два найпопулярніші:

  • url.path — це шлях у файловій системі, тобто те, що розуміє FileManager і більшість файлових API.
  • url.absoluteString — це рядок URL, тобто те, що виглядає як file:///tmp/LibraryCLI/.

І ці рядки не зобовʼязані збігатися, бо URL-рядки використовують правила екранування. Наприклад, пробіли перетворюються на %20.

Подивімося на це на живому прикладі:

import Foundation

let file = URL(fileURLWithPath: "/tmp/My Report.txt")
print(file.path)           // /tmp/My Report.txt
print(file.absoluteString) // file:///tmp/My%20Report.txt

Запамʼятайте просте правило:

absoluteString зручно «побачити очима» або покласти в діагностичний журнал як URL.
path — це те, що ви майже завжди передаєте в операції файлової системи.

Якщо їх переплутати, можете отримати «ніби правильний текст», але не той формат, який очікує інше API.

Чому URL(string:) — погана ідея для файлових шляхів

Зараз буде приклад, який економить новачкам години життя та літри чаю (або кави — тут уже як у вас із нервовою системою).

import Foundation

let a = URL(string: "/tmp/report.txt")
let b = URL(fileURLWithPath: "/tmp/report.txt")

print(a as Any)        // nil (або дивний URL, залежно від рядка)
print(b.isFileURL)     // true
print(b.path)          // /tmp/report.txt

URL(string:) намагається розбирати рядок як URL-адресу. А рядок "/tmp/report.txt" — це не URL у звичному розумінні, а шлях. Тому результат часто буде nil.

Навіть якщо URL(string:) раптом повернув щось схоже, це може бути просто вдалий збіг, а не гарантія. І такі збіги зазвичай закінчуються саме тоді, коли ви показуєте проєкт викладачеві або запускаєте його на іншій машині.

5. Збірка шляху з компонентів через URL

Тепер ми переходимо до головної практичної переваги URL. Щойно у вас є база — каталог — решту ви будуєте як конструктор, а не як рядкову арифметику.

import Foundation

let base = URL(fileURLWithPath: "/tmp", isDirectory: true)
let dataDir = base.appendingPathComponent("LibraryCLI", isDirectory: true)

print(dataDir.path) // /tmp/LibraryCLI

Зверніть увагу на isDirectory: true. Це не «обовʼязкова магія», але чудовий спосіб зробити намір читабельним: ми будуємо каталог, а не файл. Трохи пізніше, коли ми почнемо перевіряти «файл vs папка», це допоможе вам краще розуміти код.

І ще один важливий прийом: розширення файла додаємо не руками.

import Foundation

let dir = URL(fileURLWithPath: "/tmp/LibraryCLI", isDirectory: true)
let file = dir
    .appendingPathComponent("library")
    .appendingPathExtension("json")

print(file.path) // /tmp/LibraryCLI/library.json

Чому це краще, ніж "library" + ".json"? Тому що ви розділяєте «імʼя» та «розширення» і не породжуєте випадкових монстрів на кшталт library.json.json.

Мінітаблиця: шлях як String vs шлях як URL

Іноді корисно зафіксувати різницю не лише словами, а й візуально.

Що порівнюємо String (як шлях) URL (як file URL)
Складання вкладеного шляху вручну: base + "/" + name безпечно: base.appendingPathComponent(name)
Пробіли / екранування ви виправляєте це вручну URL робить коректне представлення (%20 у URL-рядку)
Розширення «перевірити, чи є крапка» appendingPathExtension, pathExtension
Явне позначення «це каталог» зазвичай неочевидно isDirectory: true у місці складання
Помилки через слеші часті майже зникають
Зручність для FileManager String потрібен, але це url.path зберігаєте URL, дістаєте path на межі

Головна ідея: усередині застосунку — URL, на межі з API — за потреби String.

Невелика схема потоку: як шлях живе в застосунку

Зараз ми зафіксуємо архітектурний принцип на рівні мінісхеми. Він ще не раз повториться в лекції.

flowchart TD
    A[Ввід користувача: String] --> B["Перетворюємо на URL(fileURLWithPath:...)"]
    B --> C[Працюємо всередині застосунку: URL]
    C --> D[Операції файлової системи: FileManager/читання/запис]
    D -->|зазвичай потрібно| E[url.path як String]
    C -->|журнал/діагностика| F[url.path або url.absoluteString]

Сенс тут простий: рядок — на вході, URL — посередині, рядок — на виході, якщо цього вимагає API.

6. LibraryCLI: допоміжна функція для побудови шляху до файла

Зараз зробимо маленький крок до нашого застосунку. Ми ще не будемо створювати каталоги і не будемо читати чи записувати файли — це теми наступних лекцій. Але вже можемо підготувати акуратну функцію, яка будує шлях до файла даних без конкатенації рядків.

Уявімо, що маємо ідею: файл бази називатиметься library.json. Поки не говоримо, де саме він лежить — політика data-dir буде пізніше, — але вже можемо навчитися будувати URL від заданого каталогу.


import Foundation

func libraryFileURL(in directory: URL) -> URL {
    // directory має бути папкою
    directory
        .appendingPathComponent("library")
        .appendingPathExtension("json")
}

let dir = URL(fileURLWithPath: "/tmp/LibraryCLI", isDirectory: true)
let file = libraryFileURL(in: dir)

print(file.path) // /tmp/LibraryCLI/library.json

Цей код маленький, але в ньому вже є дисципліна: функція робить одну річ, імʼя говорить само за себе, а всі операції над шляхом ідуть через URL-API.

Трохи пізніше ми додамо другий рівень: політику вибору каталогу, а потім підготовку каталогу через FileManager. Але фундамент — ось він.

7. Корисні властивості URL для шляхів

Дуже легко перейти до «давайте вивчимо всі методи URL», але зараз нам достатньо кількох, які справді допомагають читати код і писати менше рядкових костилів.

import Foundation

let file = URL(fileURLWithPath: "/tmp/LibraryCLI/library.json")

print(file.lastPathComponent) // library.json
print(file.pathExtension)     // json

let noExt = file.deletingPathExtension()
print(noExt.lastPathComponent) // library

Ці речі особливо корисні в логуванні та діагностиці: замість того щоб друкувати весь шлях, іноді зручно показати лише імʼя файла. І так, коли ви побачите в логах library.json.json, ви будете раді, що вмієте швидко зрозуміти, звідки це взялося.

8. Типові помилки

Помилка № 1: використовувати URL(string:) для файлового шляху.
Це стається тому, що URL(string:) здається «універсальним» і добре виглядає. Але він призначений для URL-рядків типу https://.... Для файлів завжди починайте з URL(fileURLWithPath:), інакше ви отримаєте nil або некоректне представлення адреси.

Помилка № 2: склеювати шляхи рядками, коли вже є appendingPathComponent.
Це майже завжди відбувається автоматично: ви звикли до рядків і оператора +. Проблема в тому, що чим більше ручної збірки, тим більше місць, де можна помилитися. URL-методи прибирають цілий клас багів: слеші, порожні компоненти, дивні поєднання.

Помилка № 3: плутати path і absoluteString.
На око це виглядає як «ну це ж одне й те саме, тільки з file://». Але ні: absoluteString — це URL-представлення, де можуть зʼявлятися %20 та інші екранування. У файлові операції зазвичай передають path. Якщо в майбутньому ви раптом побачите, що застосунок не знаходить файл, хоча він існує, одна з перших перевірок — що саме ви передали: path чи absoluteString.

Помилка № 4: не вказувати isDirectory: true для каталогу, а потім плутатися.
Код компілюється і без цього, тож новачок думає: «Ну й гаразд». Але далі ви починаєте будувати складніші шляхи, і читабельність падає. isDirectory: true — це не «магія», а підказка вашому майбутньому «я», яке відлагоджуватиме проєкт у пʼятницю ввечері.

Помилка № 5: зберігати шлях як String усюди, бо FileManager все одно просить рядок.
Так, частина API справді приймає String. Але це не привід перетворювати всю кодову базу на рядкову кашу. Правильний стиль — зберігати URL, а рядок (url.path) діставати лише там, де це потрібно. Це робить код більш типобезпечним і передбачуваним.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ