Заполнители типа в Swift: что это такое и когда их следует использовать +5


Заполнители типа (type placeholders) — это новая языковая фича, представленная в Swift 5.6 (Xcode 13.3).

Сама концепция очень проста - вместо указания конкретного типа мы можем поставить _ (заполнитель типа), который предписывает компилятору самому определить заполняемый тип.

В следующем примере я использую заполнитель типа для name, который впоследствии разрешается в String.

let name: _ = "Sarun"

Когда я впервые увидел эту фичу, я не сразу понял, зачем вообще она может мне понадобиться, ведь в Swift уже есть вывод типов, и я могу написать предыдущий код вообще без указания типа (или какого-либо заполнителя).

let name = "Sarun"

Зачем нужны заполнители типа

Как оказалось, простой пример выше — не совсем то, для чего были введены заполнители типа. Заполнители типа предназначены для подстановки вместо типов с несколькими типами внутри.

Вот некоторые типы, которые содержат в себе несколько типов.

Тип функции:

(parameter type) -> return type

Тип словаря:

[key type: value type]

Универсальные типы (дженерики): любой тип с более чем одним универсальным типом, например Result.

public enum Result<Success, Failure> where Failure : Error {
   case success(Success)
   case failure(Failure)
}

Проблема с типами, содержащими в себе несколько других типов заключается в том, что выведение типов Swift не справляется с такими типами; от нас требуется, чтобы мы предоставили весь тип явно, в то время как на самом деле нужна только часть этого типа. Это становится проблематичным в тех случаях, когда речь идет о более сложных типах.

Думаю, это будет проще объяснить на примерах.

Примеры

Давайте рассмотрим несколько примеров предполагаемого использования заполнителей типов.

Функции

Первый пример взят прямиком из предложения SE-0315. Мы пытаемся создать конвертер строк из Double.init.

let stringConverter = Double.init

Поскольку Double.init имеет множество перегрузок, компилятор не может предположить тип этого выражения.

До Swift 5.6 вы должны были прописывать это явно, предоставляя больше контекста для ожидаемого нами типа.

// Аннотации типа переменной
let stringConverter: (String) -> Double? = Double.init
// Приведение типа через as
let stringConverter = Double.init as (String) -> Double?

Мы предоставили как аргументы, так и возвращаемые типы, но в этом конкретном случае нам нужно уточнить только тип аргумента, поскольку существует только одна перегрузка Double.init, которая принимает String.

extension Double: LosslessStringConvertible {
public init?<S>(_ text: S) where S : StringProtocol
}

Заполнители типа позволяют нам указать только неоднозначную часть, оставив остальное на усмотрение процесса выведения типа.

let stringConverter = Double.init as (String) -> ?
// или
let stringConverter: (String) -> ? = Double.init

Несмотря на то, что этот пример указан в самом предложении, я бы не стал писать подобные вещи в своем коде. В этом конкретном случае я предпочитаю четко указать все типы.

В приведенных выше примерах мы используем ? чтобы указать, что возвращаемое значение является необязательным, но вы можете убрать оттуда вопросительный знак (?), и это не вызовет ошибки. Заполнители типов могут представлять как необязательные, так и обычные типы.

Словари

Заполнители типов можно использовать вместо ключей или значений в словарях.

Ниже мы рассмотрим пример со словарем, который хранит кэшированные изображения для каждой ячейки табличного представления, а ключами служат индексы строк.

var imageCache = [0: nil, 1: nil]

Компилятор может вывести ключ (как Int) но не значение. Максимумом, на что он способен, будет Any?.

Мы можем указать поставить на место значения заполнитель типа и оставить ключ пустым, поскольку Swift способен понять эту часть.

var imageCache: [: UIImage?] = [0: nil, 1: nil]

Опять же, я все-таки предпочитаю [Int: UIImage?] этой форме.

Универсальные типы

Именно здесь я вижу наибольшую пользу от введения заполнителей типа. Поскольку функциональное программирование становится все более популярным, может наступить время, когда, используемые нами, типы приобретут форму месива со все более сложной вложенностью, и явное указание типа или оставление его пустым (_) будет в чем-то одинаковым.

Рассмотрим следующий пример кортежа:

let weirdTuple = (0, 1, Just(1).map(.description))
// (Int, Int, Publishers.MapKeyPath<Just<Int>, String>)

Если мы используем его с перечислением Result из примера выше, как он написан сейчас, мы получим ошибку, что универсальный параметр 'Failure' не может быть выведен, и нам нужно предоставить весь тип.

let result = Result.success(weirdTuple)
// Универсальный параметр 'Failure' не может быть выведен

В этом случае, оставив тип пустым, мы сделаем код более читабельным.

let weirdTuple = (0, 1, Just(1).map(.description))
// Без заполнителя типа
let result = Result<(Int, Int, Publishers.MapKeyPath<Just<Int>, String>), Error>.success(weirdTuple)
// С заполнителем типа
let result = Result<, Error>.success(weirdTuple)

Я не могу придумать более наглядного примера, чем этот, но я надеюсь, что вы уловили суть, где это может быть очень полезно.

Опять же, заполнители типов не ограничены к применению лишь с типами, содержащими в себе другие типы. Просто лично я нахожу от них максимальную пользу именно здесь. 

Вот еще несколько примеров типов, содержащих заполнители.

Array<> // массив с заполнителем типа элемента
[Int: ] // словарь с заполнителем типа значения
() -> Int // тип функции, принимающий один аргумент-заполнитель и возвращающий 'Int'
(_, Double) // кортеж с заполнителем типа и 'Double'
? // необязательная обертка заполнителя типа

Заключение

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

Вы заполняете неоднозначную часть и все еще можете наслаждаться выводом типа с помощью .

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

let dict = [1: "One"]
dict.map { _, value in
print(value)
}

Я думаю, что эту фичу все же следует использовать с осторожностью. Я считаю, что ошибиться будет намного сложнее, когда вы четко указываете тип. Если оставлять пользователям хоть какую-нибудь неопределенность, это может привести к потенциальному недопониманию и ошибкам.

В преддверии старта курса iOS Developer. Basic хочу пригласить всех желающих на бесплатный урок, в рамках которого рассмотрим, как можно создать несложный фоторедактор для iOS для простой обработки изображений, поработаем с фильтрами и цветовыми тонами. Интерфейс приложения будем создавать с использованием UIKit Autolayout.




Комментарии (4):

  1. vadimbelyaev
    /#24341690 / +2

    Укажите, пожалуйста, что это не ваша статья, а перевод, иначе очень некрасиво получается. Оригинал вот: https://sarunw.com/posts/swift-type-placeholder/

    • MaxRokatansky
      /#24341790

      Да, конечно, указали, что это переведенный материал. Ссылка на оригинал есть в плашке перед текстом.

      • vadimbelyaev
        /#24341792 / +1

        Спасибо, теперь появилось, а на момент написания моего предыдущего комментария почему-то не отображалось.

        • MaxRokatansky
          /#24341806 / +1

          Моя ошибка, забыл поставить галочку при публикации, но быстро исправился)